Skip to content

Commit

Permalink
Add sealed_value_or_empty generator
Browse files Browse the repository at this point in the history
See #633
  • Loading branch information
thesamet committed Oct 21, 2019
1 parent afddf7f commit c7f8ae1
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 70 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -470,10 +470,19 @@ class DescriptorImplicits(params: GeneratorParams, files: Seq[FileDescriptor]) {

def parent: Option[Descriptor] = Option(message.getContainingType)

def sealedOneofStyle: SealedOneofStyle = {
assert(isSealedOneofType)
if (message.getOneofs.asScala.exists(_.getName == "sealed_value")) SealedOneofStyle.Default
else if (message.getOneofs.asScala.exists(_.getName == "sealed_value_or_empty"))
SealedOneofStyle.OrEmpty
else throw new RuntimeException("Unexpected oneof style")
}

// every message that passes this filter must be a sealed oneof. The check that it actually
// obeys the rules is done in ProtoValidation.
def isSealedOneofType: Boolean = {
message.getOneofs.asScala.exists(_.getName == "sealed_value")
message.getOneofs.asScala
.exists(o => o.getName == "sealed_value" || o.getName == "sealed_value_or_empty")
}

def isSealedOneofCase: Boolean = sealedOneofsCache.getContainer(message).isDefined
Expand Down Expand Up @@ -552,12 +561,14 @@ class DescriptorImplicits(params: GeneratorParams, files: Seq[FileDescriptor]) {

def sealedOneofName = {
require(isSealedOneofType)
scalaName.stripSuffix(OneofMessageSuffix).asSymbol
sealedOneofStyle match {
case SealedOneofStyle.Default => scalaName.stripSuffix(OneofMessageSuffix)
case SealedOneofStyle.OrEmpty =>
(scalaName.stripSuffix(OneofMessageSuffix) + "OrEmpty")
}
}

def sealedOneofNameSymbol = {
sealedOneofName.asSymbol
}
def sealedOneofNameSymbol = sealedOneofName.asSymbol

def sealedOneofScalaType = {
parent match {
Expand All @@ -567,7 +578,15 @@ class DescriptorImplicits(params: GeneratorParams, files: Seq[FileDescriptor]) {
}
}

def sealedOneofNonEmptyScalaType = sealedOneofScalaType + ".NonEmpty"
def sealedOneofNonEmptyName = sealedOneofStyle match {
case SealedOneofStyle.Default => "NonEmpty"
case SealedOneofStyle.OrEmpty => sealedOneofName.stripSuffix("OrEmpty")
}

def sealedOneofNonEmptyScalaType = sealedOneofStyle match {
case SealedOneofStyle.Default => sealedOneofScalaType + "." + sealedOneofNonEmptyName.asSymbol
case SealedOneofStyle.OrEmpty => sealedOneofScalaType.stripSuffix("OrEmpty")
}

private[this] val valueClassNames = Set("AnyVal", "scala.AnyVal", "_root_.scala.AnyVal")

Expand Down Expand Up @@ -633,8 +652,11 @@ class DescriptorImplicits(params: GeneratorParams, files: Seq[FileDescriptor]) {
specialMixins
}

def sealedOneofBaseClasses: Seq[String] =
s"scalapb.GeneratedSealedOneof" +: messageOptions.getSealedOneofExtendsList.asScala.toSeq
def sealedOneofBaseClasses: Seq[String] = sealedOneofStyle match {
case SealedOneofStyle.Default =>
s"scalapb.GeneratedSealedOneof" +: messageOptions.getSealedOneofExtendsList.asScala.toSeq
case SealedOneofStyle.OrEmpty => messageOptions.getSealedOneofExtendsList.asScala.toSeq
}

def nestedTypes: Seq[Descriptor] = message.getNestedTypes.asScala.toSeq

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1004,7 +1004,10 @@ class ProtobufGenerator(
s"__fieldsMap.getOrElse(__fields.get(${field.getIndex}), $t).asInstanceOf[$baseTypeName]"
}

s"${field.scalaName.asSymbol} = " + transform(field).apply(e, enclosingType = field.enclosingType)
s"${field.scalaName.asSymbol} = " + transform(field).apply(
e,
enclosingType = field.enclosingType
)
}
val oneOfs = message.getOneofs.asScala.map { oneOf =>
val elems = oneOf.fields.map { field =>
Expand Down Expand Up @@ -1075,7 +1078,10 @@ class ProtobufGenerator(
s"$value.map(_.as[$baseTypeName]).getOrElse($t)"
}

s"${field.scalaName.asSymbol} = " + transform(field).apply(e, enclosingType = field.enclosingType)
s"${field.scalaName.asSymbol} = " + transform(field).apply(
e,
enclosingType = field.enclosingType
)
}
val oneOfs = message.getOneofs.asScala.map { oneOf =>
val elems = oneOf.fields.map { field =>
Expand Down Expand Up @@ -1493,68 +1499,9 @@ class ProtobufGenerator(
fp.add(asScalaDocBlock(mainDoc ++ sep ++ fieldsDoc): _*)
}

def generateSealedOneofTrait(message: Descriptor): PrinterEndo = { fp =>
if (!message.isSealedOneofType) fp
else {
val baseType = message.scalaTypeName
val sealedOneOfType = message.sealedOneofScalaType
val sealedOneOfNonEmptyType = message.sealedOneofNonEmptyScalaType
val sealedOneofName = message.sealedOneofNameSymbol
val typeMapper = s"_root_.scalapb.TypeMapper[${baseType}, ${sealedOneOfType}]"
val oneof = message.getOneofs.get(0)
val typeMapperName = { message.sealedOneofName } + "TypeMapper"
fp.add(
s"sealed trait $sealedOneofName extends ${message.sealedOneofBaseClasses.mkString(" with ")} {"
)
.addIndented(
s"type MessageType = $baseType",
s"final def isEmpty = this.isInstanceOf[${sealedOneOfType}.Empty.type]",
s"final def isDefined = !isEmpty",
s"final def asMessage: $baseType = ${message.sealedOneofScalaType}.$typeMapperName.toBase(this)",
s"final def asNonEmpty: Option[$sealedOneOfNonEmptyType] = if (isEmpty) None else Some(this.asInstanceOf[$sealedOneOfNonEmptyType])"
)
.add("}")
.add("")
.add(s"object $sealedOneofName {")
.indented(
_.add(
s"case object Empty extends $sealedOneOfType",
"",
s"sealed trait NonEmpty extends $sealedOneOfType",
"",
s"def defaultInstance: ${sealedOneOfType} = Empty",
"",
s"implicit val $typeMapperName: $typeMapper = new $typeMapper {"
).indented(
_.add(
s"override def toCustom(__base: $baseType): $sealedOneOfType = __base.${oneof.scalaName} match {"
).indented(
_.print(oneof.fields) {
case (fp, field) =>
fp.add(s"case __v: ${field.oneOfTypeName} => __v.value")
}.add(s"case ${oneof.scalaTypeName}.Empty => Empty")
)
.add("}")
.add(
s"override def toBase(__custom: $sealedOneOfType): $baseType = $baseType(__custom match {"
)
.indented(
_.print(oneof.fields) {
case (fp, field) =>
fp.add(s"case __v: ${field.scalaTypeName} => ${field.oneOfTypeName}(__v)")
}.add(s"case Empty => ${oneof.scalaTypeName}.Empty")
)
.add("})")
)
.add("}")
)
.add("}")
}
}

def printMessage(printer: FunctionalPrinter, message: Descriptor): FunctionalPrinter = {
printer
.call(generateSealedOneofTrait(message))
.call(new SealedOneofsGenerator(message, implicits).generateSealedOneofTrait)
.call(generateScalaDoc(message))
.add(s"@SerialVersionUID(0L)")
.seq(message.annotationList)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package scalapb.compiler

import scalapb.compiler.FunctionalPrinter.PrinterEndo
import com.google.protobuf.Descriptors._

sealed trait SealedOneofStyle

object SealedOneofStyle {
case object Default extends SealedOneofStyle

case object OrEmpty extends SealedOneofStyle
}

class SealedOneofsGenerator(message: Descriptor, implicits: DescriptorImplicits) {
import implicits._

def generateSealedOneofTrait: PrinterEndo = { fp =>
if (!message.isSealedOneofType) fp
else {
val baseType = message.scalaTypeName
val sealedOneofType = message.sealedOneofScalaType
val sealedOneofNonEmptyName = message.sealedOneofNonEmptyName
val sealedOneofNonEmptyType = message.sealedOneofNonEmptyScalaType
val sealedOneofName = message.sealedOneofNameSymbol
val typeMapper = s"_root_.scalapb.TypeMapper[${baseType}, ${sealedOneofType}]"
val oneof = message.getOneofs.get(0)
val typeMapperName = message.sealedOneofNameSymbol + "TypeMapper"
val nonEmptyTopLevel = message.sealedOneofStyle == SealedOneofStyle.OrEmpty

def addNonEmptySealedTrait: PrinterEndo = _.add(
s"sealed trait $sealedOneofNonEmptyName extends $sealedOneofType",
""
)

fp.add(
s"sealed trait $sealedOneofName extends ${message.sealedOneofBaseClasses.mkString(" with ")} {"
)
.addIndented(
s"type MessageType = $baseType",
s"final def isEmpty = this.isInstanceOf[${sealedOneofType}.Empty.type]",
s"final def isDefined = !isEmpty",
s"final def asMessage: $baseType = ${message.sealedOneofScalaType}.$typeMapperName.toBase(this)",
s"final def asNonEmpty: Option[$sealedOneofNonEmptyType] = if (isEmpty) None else Some(this.asInstanceOf[$sealedOneofNonEmptyType])"
)
.add("}")
.add("")
.when(nonEmptyTopLevel)(addNonEmptySealedTrait)
.add(s"object $sealedOneofName {")
.indented(
_.add(s"case object Empty extends $sealedOneofType", "")
.when(!nonEmptyTopLevel)(addNonEmptySealedTrait)
.add(
s"def defaultInstance: ${sealedOneofType} = Empty",
"",
s"implicit val $typeMapperName: $typeMapper = new $typeMapper {"
)
.indented(
_.add(
s"override def toCustom(__base: $baseType): $sealedOneofType = __base.${oneof.scalaName} match {"
).indented(
_.print(oneof.fields) {
case (fp, field) =>
fp.add(s"case __v: ${field.oneOfTypeName} => __v.value")
}.add(s"case ${oneof.scalaTypeName}.Empty => Empty")
)
.add("}")
.add(
s"override def toBase(__custom: $sealedOneofType): $baseType = $baseType(__custom match {"
)
.indented(
_.print(oneof.fields) {
case (fp, field) =>
fp.add(s"case __v: ${field.scalaTypeName} => ${field.oneOfTypeName}(__v)")
}.add(s"case Empty => ${oneof.scalaTypeName}.Empty")
)
.add("})")
)
.add("}")
)
.add("}")
}
}
}
25 changes: 25 additions & 0 deletions e2e/src/main/protobuf/sealed_oneof_or_empty.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
syntax = "proto3";

package com.thesamet.proto.e2e.or_empty;

message Lit {
int32 value = 1;
}

message Add {
Expr lhs = 1;
Expr rhs = 2;
}

message Expr {
oneof sealed_value_or_empty {
Lit lit = 1;
Add add = 2;
}
}

message Programs {
repeated Expr programs = 1;
Expr optional_expr = 2;
map<string, Expr> expr_map = 3;
}
9 changes: 9 additions & 0 deletions e2e/src/test/scala/SealedOneofSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,13 @@ class SealedOneofSpec extends FlatSpec with MustMatchers {
case None => ""
}
}

"or-empty sealed oneofs" should "Work" in {
import com.thesamet.proto.e2e.or_empty.{sealed_oneof_or_empty => OO}
OO.Programs(optionalExpr = OO.ExprOrEmpty.Empty)
OO.Programs(optionalExpr = OO.Lit(32))
OO.ExprOrEmpty.Empty.isEmpty
OO.Lit(32).isEmpty must be(false)
OO.Lit(32).asNonEmpty must be(OO.Lit(32))
}
}

0 comments on commit c7f8ae1

Please sign in to comment.