Skip to content

Commit

Permalink
reinstate PROTO3 Option support with an implicit (#127)
Browse files Browse the repository at this point in the history
* reinstate PROTO3 Option support with an implicit

* reorg
  • Loading branch information
nevillelyh committed May 15, 2020
1 parent 8d0863e commit 78c7a7e
Show file tree
Hide file tree
Showing 4 changed files with 52 additions and 6 deletions.
2 changes: 1 addition & 1 deletion docs/protobuf.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ val copy: Outer = protobufType.from(proto)

Additional `ProtobufField[T]` instances for `Byte`, `Char`, and `Short` are available from `import magnolify.protobuf.unsafe._`. These conversions are unsafe due to potential overflow.

Nullable type `Option[T]` is not supported when `MsgT` is compiled with Protobuf 3 syntax. This is because Protobuf 3 does not offer a way to check if a field was set, and instead returns `0`, `""`, `false`, etc. when it was not.
By default nullable type `Option[T]` is not supported when `MsgT` is compiled with Protobuf 3 syntax. This is because Protobuf 3 does not offer a way to check if a field was set, and instead returns `0`, `""`, `false`, etc. when it was not. You can enable Protobuf 3 support for `Option[T]` by adding `import magnolify.protobuf.unsafe.Proto3Option._`. However with this, Scala `None`s will become `0/""/false` in Protobuf and come back as `Some(0/""/false)`.
26 changes: 24 additions & 2 deletions protobuf/src/main/scala/magnolify/protobuf/ProtobufType.scala
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,32 @@ sealed trait ProtobufType[T, MsgT <: Message] extends Converter[T, MsgT, MsgT] {
def apply(t: T): MsgT = to(t)
}

sealed trait ProtobufOption {
def check(f: ProtobufField.Record[_], syntax: Syntax): Unit
}

object ProtobufOption {
implicit val proto2Option: ProtobufOption = new ProtobufOption {
override def check(f: ProtobufField.Record[_], syntax: Syntax): Unit =
if (f.hasOptional) {
require(
syntax == Syntax.PROTO2,
"Option[T] support is PROTO2 only, " +
"`import magnolify.protobuf.unsafe.Proto3Option._` to enable PROTO3 support"
)
}
}

private[protobuf] class Proto3Option extends ProtobufOption {
override def check(f: ProtobufField.Record[_], syntax: Syntax): Unit = ()
}
}

object ProtobufType {
implicit def apply[T, MsgT <: Message](implicit
f: ProtobufField.Record[T],
ct: ClassTag[MsgT]
ct: ClassTag[MsgT],
po: ProtobufOption
): ProtobufType[T, MsgT] =
new ProtobufType[T, MsgT] {
if (f.hasOptional) {
Expand All @@ -50,7 +72,7 @@ object ProtobufType {
.asInstanceOf[Descriptor]
.getFile
.getSyntax
require(syntax == Syntax.PROTO2, "Option[T] support is PROTO2 only")
po.check(f, syntax)
}

@transient private var _newBuilder: Method = _
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,8 @@ package object unsafe {
implicit val pfByte = ProtobufField.from[Int](_.toByte)(_.toInt)
implicit val pfChar = ProtobufField.from[Int](_.toChar)(_.toInt)
implicit val pfShort = ProtobufField.from[Int](_.toShort)(_.toInt)

object Proto3Option {
implicit val proto3Option: ProtobufOption = new ProtobufOption.Proto3Option
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,37 @@ object ProtobufTypeSpec extends MagnolifySpec("ProtobufType") {

test[Integers, IntegersP2]
test[Integers, IntegersP3]
// PROTO3 removes the notion of require vs optional fields.
// The new singular field returns default value if unset, making it required essentially.
test[Required, RequiredP2]
test[Required, SingularP3]
test[Nullable, NullableP2]

// PROTO3 removes the notion of require vs optional fields.
// By default `Option[T] are not supported`.
try {
test[Nullable, SingularP3]
} catch {
case e: IllegalArgumentException =>
require(e.getMessage == "requirement failed: Option[T] support is PROTO2 only")
require(
e.getMessage ==
"requirement failed: Option[T] support is PROTO2 only, " +
"`import magnolify.protobuf.unsafe.Proto3Option._` to enable PROTO3 support"
)
}

// Adding `import magnolify.protobuf.unsafe.Proto3Option._` enables PROTO3 `Option[T]` support.
// The new singular field returns default value if unset.
// Hence `None` round trips back as `Some(false/0/"")`.
{
import magnolify.protobuf.unsafe.Proto3Option._
val eq = Eq.instance[Nullable] { (x, y) =>
x.b.getOrElse(false) == y.b.getOrElse(false) &&
x.i.getOrElse(0) == y.i.getOrElse(0) &&
x.s.getOrElse("") == y.s.getOrElse("")
}
val arb = implicitly[Arbitrary[Nullable]]
test(classTag[Nullable], arb, classTag[SingularP3], ProtobufType[Nullable, SingularP3], eq)
}

test[Repeated, RepeatedP2]
test[Repeated, RepeatedP3]
test[Nested, NestedP2]
Expand Down

0 comments on commit 78c7a7e

Please sign in to comment.