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

reinstate PROTO3 Option support with an implicit #127

Merged
merged 2 commits into from
May 15, 2020
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 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