Skip to content

Commit

Permalink
Add new option for further customization of collections
Browse files Browse the repository at this point in the history
In order to support Collections that are quite different than the ones
in the standard library, we add a new `collection` option for repeated
fields. The collection can reference an `Adapter` type available at
runtime that will forward calls for an underlying collection.

This helps support types such as cats.data.NonEmptyList, NonEmptySet
and NonEmptyMap. For example, those collections don't define `foreach`,
and `++` where the rhs is an iterable. For `NonEmptySet` there is even
no `size` method.

See #1013 and scalapb/scalapb-validate#38
  • Loading branch information
thesamet committed Dec 25, 2020
1 parent 0e913ec commit c9ece97
Show file tree
Hide file tree
Showing 23 changed files with 689 additions and 117 deletions.
4 changes: 3 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -306,10 +306,12 @@ lazy val e2e = (project in file("e2e"))
.dependsOn(grpcRuntime)
.settings(e2eCommonSettings)
.settings(
libraryDependencies += "org.typelevel" %% "cats-core" % "2.3.0",
scalacOptions ++= Seq(
"-P:silencer:globalFilters=eprecatedInt32 in class TestDeprecatedFields is deprecated",
"-P:silencer:pathFilters=custom_options_use;CustomAnnotationProto.scala;changed/scoped;ServerReflectionGrpc.scala",
"-P:silencer:lineContentFilters=import com.thesamet.pb.MisplacedMapper.weatherMapper"
"-P:silencer:lineContentFilters=import com.thesamet.pb.MisplacedMapper.weatherMapper",
"-P:silencer:lineContentFilters=import Ordering"
),
Compile / PB.protoSources += (Compile / PB.externalIncludePath).value / "grpc" / "reflection",
Compile / PB.generate := ((Compile / PB.generate) dependsOn (protocGenScalaUnix / Compile / assembly)).value,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,14 +254,14 @@ class DescriptorImplicits(params: GeneratorParams, files: Seq[FileDescriptor]) {
if (isSingular) EnclosingType.None
else if (supportsPresence || fd.isInOneof) EnclosingType.ScalaOption
else {
EnclosingType.Collection(collectionType)
EnclosingType.Collection(collectionType, collection.adapter)
}

def fieldMapEnclosingType: EnclosingType =
if (isSingular) EnclosingType.None
else if (supportsPresence || fd.isInOneof) EnclosingType.ScalaOption
else if (!fd.isMapField) EnclosingType.Collection(collectionType)
else EnclosingType.Collection(ScalaSeq)
else if (!fd.isMapField) EnclosingType.Collection(collectionType, collection.adapter)
else EnclosingType.Collection(ScalaSeq, None)

def isMapField = isMessage && fd.isRepeated && fd.getMessageType.isMapEntry

Expand All @@ -270,30 +270,73 @@ class DescriptorImplicits(params: GeneratorParams, files: Seq[FileDescriptor]) {
fd.getMessageType.mapType
}

def collectionBuilder: String = {
require(fd.isRepeated)
val t = if (collectionType == ScalaSeq) ScalaVector else collectionType
class CollectionHelpers {
def newBuilder: String = {
val t = if (collectionType == ScalaSeq) ScalaVector else collectionType

if (!fd.isMapField)
s"$t.newBuilder[$singleScalaTypeName]"
else {
s"$t.newBuilder[${fd.mapType.keyType}, ${fd.mapType.valueType}]"
if (!fd.isMapField) {
adapter match {
case None => s"$t.newBuilder[$singleScalaTypeName]"
case Some(tc) => s"$tc.newBuilder[$singleScalaTypeName]"
}
} else {
adapter match {
case None => s"$t.newBuilder[${fd.mapType.keyType}, ${fd.mapType.valueType}]"
case Some(tc) => s"$tc.newBuilder[${fd.mapType.keyType}, ${fd.mapType.valueType}]"
}
}
}

def empty: String = adapter match {
case None => s"${collectionType}.empty"
case Some(tc) => s"$tc.empty"
}

def foreach = adapter match {
case None => fd.scalaName.asSymbol + ".foreach"
case Some(tc) => s"$tc.foreach(${fd.scalaName.asSymbol})"
}

def concat(left: String, right: String) = adapter match {
case None => s"$left ++ $right"
case Some(tc) => s"$tc.concat($left, $right)"
}
}

def emptyCollection: String = {
s"${collectionType}.empty"
def nonEmptyType = fd.fieldOptions.getCollection.getNonEmpty

def nonEmptyCheck(expr: String) = if (nonEmptyType) "true" else s"$expr.nonEmpty"

def adapter: Option[String] = {
if (fd.fieldOptions.getCollection.hasAdapter())
Some(fd.fieldOptions.getCollection.getAdapter())
else None
}


def size: Expression = adapter match {
case None => MethodApplication("size")
case Some(tc) => FunctionApplication(s"$tc.size")
}

def iterator(e: String): String = adapter match {
case None => s"$e.iterator"
case Some(tc) => s"$tc.toIterator($e)"
}
}

def collection: CollectionHelpers = new CollectionHelpers

// In scalapb.proto, we separate between collection_type and map_type, but internally this is unified.
def collectionType: String = {
require(fd.isRepeated)
if (fd.isMapField) {
if (fd.fieldOptions.hasMapType) fd.fieldOptions.getMapType
if (fd.fieldOptions.getCollection.hasType) fd.fieldOptions.getCollection.getType
else if (fd.fieldOptions.hasMapType) fd.fieldOptions.getMapType
else if (fd.getFile.scalaOptions.hasMapType) fd.getFile.scalaOptions.getMapType
else ScalaMap
} else {
if (fd.fieldOptions.hasCollectionType) fd.fieldOptions.getCollectionType
if (fd.fieldOptions.getCollection.hasType) fd.fieldOptions.getCollection.getType
else if (fd.fieldOptions.hasCollectionType) fd.fieldOptions.getCollectionType
else if (fd.getFile.scalaOptions.hasCollectionType)
fd.getFile.scalaOptions.getCollectionType
else ScalaSeq
Expand All @@ -302,15 +345,15 @@ class DescriptorImplicits(params: GeneratorParams, files: Seq[FileDescriptor]) {

def fieldMapCollection(innerType: String) = {
if (supportsPresence) s"_root_.scala.Option[$innerType]"
else if (fd.isRepeated && !fd.isMapField) s"${collectionType}[$innerType]"
else if (fd.isRepeated && fd.isMapField) s"${ScalaSeq}[$innerType]"
else if (fd.isRepeated && !fd.isMapField) s"${collectionType}[$innerType]"
else innerType
}

def fieldsMapEmptyCollection: String = {
require(fd.isRepeated)
if (fd.isMapField) s"$ScalaSeq.empty"
else emptyCollection
else collection.empty
}

def scalaTypeName: String =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,22 @@ sealed trait Expression extends Product with Serializable {
case (e1: LiteralExpression, e2: LiteralExpression) => ExpressionList(e2 :: e1 :: Nil)
}

def apply(e: String, enclosingType: EnclosingType, mustCopy: Boolean = false): String =
ExpressionBuilder.run(this)(e, enclosingType, mustCopy)
def apply(
e: String,
sourceType: EnclosingType,
targetType: EnclosingType,
mustCopy: Boolean
): String =
ExpressionBuilder.run(this, e, sourceType, targetType, mustCopy)

def apply(e: String, sourceType: EnclosingType, targetType: EnclosingType): String =
ExpressionBuilder.run(this, e, sourceType, targetType, false)

def apply(e: String, sourceType: EnclosingType, mustCopy: Boolean): String =
ExpressionBuilder.run(this, e, sourceType, sourceType, mustCopy)

def apply(e: String, sourceType: EnclosingType): String =
ExpressionBuilder.run(this, e, sourceType, sourceType, false)
}

case class ExpressionList(l: List[LiteralExpression]) extends Expression
Expand Down Expand Up @@ -51,64 +65,80 @@ object ExpressionBuilder {
case OperatorApplication(name) :: tail => s"${runSingleton(tail)(e)} $name"
}

def convertCollection(expr: String, enclosingType: EnclosingType): String = {
val convert = List(enclosingType match {
case Collection(DescriptorImplicits.ScalaVector) => MethodApplication("toVector")
case Collection(DescriptorImplicits.ScalaSeq) => MethodApplication("toSeq")
case Collection(DescriptorImplicits.ScalaMap) => MethodApplication("toMap")
case Collection(DescriptorImplicits.ScalaIterable) =>
def convertCollection(expr: String, targetType: EnclosingType): String = {
val convert = List(targetType match {
case Collection(_, Some(tc)) => FunctionApplication(s"${tc}.fromIterator")
case Collection(DescriptorImplicits.ScalaVector, _) => MethodApplication("toVector")
case Collection(DescriptorImplicits.ScalaSeq, _) => MethodApplication("toSeq")
case Collection(DescriptorImplicits.ScalaMap, _) => MethodApplication("toMap")
case Collection(DescriptorImplicits.ScalaIterable, _) =>
FunctionApplication("_root_.scalapb.internal.compat.toIterable")
case Collection(_) => FunctionApplication("_root_.scalapb.internal.compat.convertTo")
case _ => Identity
case Collection(_, _) => FunctionApplication("_root_.scalapb.internal.compat.convertTo")
case _ => Identity
})
runSingleton(convert)(expr)
}

def runCollection(
es: List[LiteralExpression]
)(e0: String, enclosingType: EnclosingType, mustCopy: Boolean): String = {
require(enclosingType != EnclosingType.None)
)(e0: String, sourceType: EnclosingType, targetType: EnclosingType, mustCopy: Boolean): String = {
require(sourceType != EnclosingType.None)
val nontrivial: List[LiteralExpression] = es.filterNot(_.isIdentity)
val needVariable =
nontrivial
.filterNot(_.isIdentity)
.dropRight(1)
.exists(_.isFunctionApplication)

val e = if (enclosingType.isInstanceOf[Collection]) {
e0 + ".iterator"
} else e0
val e = sourceType match {
case Collection(_, Some(tc)) => s"$tc.toIterator($e0)"
case Collection(_, None) => s"$e0.iterator"
case _ => e0
}

val forceTypeConversion = sourceType match {
case Collection(_, Some(_)) if sourceType != targetType => true
case _ => false
}

if (needVariable)
convertCollection(s"""$e.map(__e => ${runSingleton(nontrivial)("__e")})""", enclosingType)
convertCollection(s"""$e.map(__e => ${runSingleton(nontrivial)("__e")})""", targetType)
else if (nontrivial.nonEmpty) {
val f = nontrivial match {
case List(FunctionApplication(name)) =>
name
case _ =>
runSingleton(nontrivial)("_")
}
convertCollection(s"""$e.map($f)""", enclosingType)
convertCollection(s"""$e.map($f)""", targetType)
} else if (mustCopy) {
convertCollection(s"""$e.map(_root_.scala.Predef.identity)""", enclosingType)
convertCollection(s"""$e.map(_root_.scala.Predef.identity)""", targetType)
} else if (forceTypeConversion) {
convertCollection(e, targetType)
} else e0
}

def run(
es: List[LiteralExpression]
)(e: String, enclosingType: EnclosingType, mustCopy: Boolean): String =
enclosingType match {
private[scalapb] def run(
es: List[LiteralExpression], e: String, sourceType: EnclosingType, targetType: EnclosingType, mustCopy: Boolean): String =
sourceType match {
case EnclosingType.None =>
runSingleton(es)(e)
case _ =>
runCollection(es)(e, enclosingType, mustCopy)
runCollection(es)(e, sourceType, targetType, mustCopy)
}

def run(es: Expression)(e: String, enclosingType: EnclosingType, mustCopy: Boolean): String =
private[scalapb] def run(
es: Expression, e: String, sourceType: EnclosingType, targetType: EnclosingType, mustCopy: Boolean): String =
es match {
case ExpressionList(l) => run(l)(e, enclosingType, mustCopy)
case expr: LiteralExpression => run(expr :: Nil)(e, enclosingType, mustCopy)
case ExpressionList(l) => run(l, e, sourceType, targetType, mustCopy)
case expr: LiteralExpression => run(expr :: Nil, e, sourceType, targetType, mustCopy)
}

@deprecated("0.10.10", "Use Expression.run")
def run(
es: Expression
)(e: String, sourceType: EnclosingType, mustCopy: Boolean): String =
run(es, e, sourceType, sourceType, mustCopy)
}

sealed trait EnclosingType
Expand All @@ -119,5 +149,7 @@ object EnclosingType {

/** Indicates that the result should be a collection with type constructor cc, such as List, Map.
*/
case class Collection(cc: String) extends EnclosingType
case class Collection(cc: String, typeClass: Option[String]) extends EnclosingType {
def this(cc: String) { this(cc, scala.None) }
}
}
Loading

0 comments on commit c9ece97

Please sign in to comment.