diff --git a/docs/mapping.md b/docs/mapping.md index 91fb93ba0..bafde4b65 100644 --- a/docs/mapping.md +++ b/docs/mapping.md @@ -2,38 +2,37 @@ | Scala | Avro | BigQuery | Bigtable7 | Datastore | Parquet | Protobuf | TensorFlow | |-----------------------------------|------------------------------|------------------------|---------------------------------|-----------------------|-----------------------------------|-------------------------|---------------------| -| `Unit` | `NULL` | x | x | `Null` | x | x | x | -| `Boolean` | `BOOLEAN` | `BOOL` | `Byte` | `Boolean` | `BOOLEAN` | `Boolean` | `INT64`3 | -| `Char` | `INT`3 | `INT64`3 | `Char` | `Integer`3 | `INT32`3 | `Int`3 | `INT64`3 | -| `Byte` | `INT`3 | `INT64`3 | `Byte` | `Integer`3 | `INT32`9 | `Int`3 | `INT64`3 | -| `Short` | `INT`3 | `INT64`3 | `Short` | `Integer`3 | `INT32`9 | `Int`3 | `INT64`3 | -| `Int` | `INT` | `INT64`3 | `Int` | `Integer`3 | `INT32`9 | `Int` | `INT64`3 | -| `Long` | `LONG` | `INT64` | `Long` | `Integer` | `INT64`9 | `Long` | `INT64` | -| `Float` | `FLOAT` | `FLOAT64`3 | `Float` | `Double`3 | `FLOAT` | `Float` | `FLOAT` | -| `Double` | `DOUBLE` | `FLOAT64` | `Double` | `Double` | `DOUBLE` | `Double` | `FLOAT`3 | -| `CharSequence` | `STRING` | x | x | x | x | x | x | -| `String` | `STRING` | `STRING` | `String` | `String` | `BINARY` | `String` | `BYTES`3 | -| `Array[Byte]` | `BYTES` | `BYTES` | `ByteString` | `Blob` | `BINARY` | `ByteString` | `BYTES` | +| `Unit` | `null` | x | x | `Null` | x | x | x | +| `Boolean` | `boolean` | `BOOL` | `Byte` | `Boolean` | `BOOLEAN` | `Boolean` | `INT64`3 | +| `Char` | `int`3 | `INT64`3 | `Char` | `Integer`3 | `INT32`3 | `Int`3 | `INT64`3 | +| `Byte` | `int`3 | `INT64`3 | `Byte` | `Integer`3 | `INT32`9 | `Int`3 | `INT64`3 | +| `Short` | `int`3 | `INT64`3 | `Short` | `Integer`3 | `INT32`9 | `Int`3 | `INT64`3 | +| `Int` | `int` | `INT64`3 | `Int` | `Integer`3 | `INT32`9 | `Int` | `INT64`3 | +| `Long` | `long` | `INT64` | `Long` | `Integer` | `INT64`9 | `Long` | `INT64` | +| `Float` | `float` | `FLOAT64`3 | `Float` | `Double`3 | `FLOAT` | `Float` | `FLOAT` | +| `Double` | `double` | `FLOAT64` | `Double` | `Double` | `DOUBLE` | `Double` | `FLOAT`3 | +| `CharSequence` | `string` | x | x | x | x | x | x | +| `String` | `string` | `STRING` | `String` | `String` | `BINARY` | `String` | `BYTES`3 | +| `Array[Byte]` | `bytes` | `BYTES` | `ByteString` | `Blob` | `BINARY` | `ByteString` | `BYTES` | | `ByteString` | x | x | `ByteString` | `Blob` | x | `ByteString` | `BYTES` | -| `ByteBuffer` | `BYTES` | x | x | | x | x | x | -| Enum1 | `ENUM` | `STRING`3 | `String` | `String`3 | `BINARY`/`ENUM`9 | Enum | `BYTES`3 | +| `ByteBuffer` | `bytes` | x | x | | x | x | x | +| Enum1 | `enum` | `STRING`3 | `String` | `String`3 | `BINARY`/`ENUM`9 | Enum | `BYTES`3 | | `BigInt` | x | x | `BigInt` | x | x | x | x | -| `BigDecimal` | `BYTES`4 | `NUMERIC`6 | `Int` scale + unscaled `BigInt` | x | `LOGICAL[DECIMAL]`9,14 | x | x | -| `Option[T]` | `UNION[NULL, T]`5 | `NULLABLE` | Empty as `None` | Absent as `None` | `OPTIONAL` | `optional`10 | Size <= 1 | -| `Iterable[T]`2 | `ARRAY` | `REPEATED` | x | `Array` | `REPEATED`13 | `repeated` | Size >= 0 | -| Nested | `RECORD` | `STRUCT` | Flat8 | `Entity` | Group | `Message` | Flat8 | -| `Map[CharSequence, T]` | `MAP[STRING, T]` | x | x | x | x | x | | -| `Map[String, T]` | `MAP[STRING, T]` | x | x | x | x | x | x | -| `java.time.Instant` | `LONG`11 | `TIMESTAMP` | x | `Timestamp` | `LOGICAL[TIMESTAMP]`9 | x | x | -| `java.time.LocalDateTime` | `LONG`11 | `DATETIME` | x | x | `LOGICAL[TIMESTAMP]`9 | x | x | +| `BigDecimal` | `bytes`4 | `NUMERIC`6 | `Int` scale + unscaled `BigInt` | x | `LOGICAL[DECIMAL]`9,14 | x | x | +| `Option[T]` | `union[null, T]`5 | `NULLABLE` | Empty as `None` | Absent as `None` | `OPTIONAL` | `optional`10 | Size <= 1 | +| `Iterable[T]`2 | `array[T]` | `REPEATED` | x | `Array` | `REPEATED`13 | `repeated` | Size >= 0 | +| Nested | `record` | `STRUCT` | Flat8 | `Entity` | Group | `Message` | Flat8 | +| `Map[K, V]` | `map[V]`15 | x | x | x | x | `map` | x | +| `java.time.Instant` | `long`11 | `TIMESTAMP` | x | `Timestamp` | `LOGICAL[TIMESTAMP]`9 | x | x | +| `java.time.LocalDateTime` | `long`11 | `DATETIME` | x | x | `LOGICAL[TIMESTAMP]`9 | x | x | | `java.time.OffsetTime` | x | x | x | x | `LOGICAL[TIME]`9 | x | x | -| `java.time.LocalTime` | `LONG`11 | `TIME` | x | x | `LOGICAL[TIME]`9 | x | x | -| `java.time.LocalDate` | `INT`11 | `DATE` | x | x | `LOGICAL[DATE]`9 | x | x | -| `org.joda.time.LocalDate` | `INT`11 | x | x | x | x | x | x | -| `org.joda.time.DateTime` | `INT`11 | x | x | x | x | x | x | -| `org.joda.time.LocalTime` | `INT`11 | x | x | x | x | x | x | -| `java.util.UUID` | `STRING`4 | x | ByteString (16 bytes) | x | `FIXED[16]` | x | x | -| `(Long, Long, Long)`12 | `FIXED[12]` | x | x | x | x | x | x | +| `java.time.LocalTime` | `long`11 | `TIME` | x | x | `LOGICAL[TIME]`9 | x | x | +| `java.time.LocalDate` | `int`11 | `DATE` | x | x | `LOGICAL[DATE]`9 | x | x | +| `org.joda.time.LocalDate` | `int`11 | x | x | x | x | x | x | +| `org.joda.time.DateTime` | `int`11 | x | x | x | x | x | x | +| `org.joda.time.LocalTime` | `int`11 | x | x | x | x | x | x | +| `java.util.UUID` | `string`4 | x | ByteString (16 bytes) | x | `FIXED[16]` | x | x | +| `(Long, Long, Long)`12 | `fixed[12]` | x | x | x | x | x | x | 1. Those wrapped in`UnsafeEnum` are encoded as strings, see [enums.md](https://github.com/spotify/magnolify/blob/master/docs/enums.md) for more @@ -59,3 +58,4 @@ format: `required group $FIELDNAME (LIST) { repeated $FIELDTYPE array ($FIELDSCHEMA); }`. 14. Parquet's Decimal logical format supports multiple representations, and are not implicitly scoped by default. Import one of: `magnolify.parquet.ParquetField.{decimal32, decimal64, decimalFixed, decimalBinary}`. +15. Map key type in avro is fixed to string. Scala Map key type must be either `String` or `CharSequence`. \ No newline at end of file diff --git a/protobuf/src/main/scala/magnolify/protobuf/ProtobufType.scala b/protobuf/src/main/scala/magnolify/protobuf/ProtobufType.scala index e83572813..b59953833 100644 --- a/protobuf/src/main/scala/magnolify/protobuf/ProtobufType.scala +++ b/protobuf/src/main/scala/magnolify/protobuf/ProtobufType.scala @@ -19,7 +19,7 @@ package magnolify.protobuf import java.lang.reflect.Method import java.util as ju import com.google.protobuf.Descriptors.{Descriptor, EnumValueDescriptor, FieldDescriptor} -import com.google.protobuf.{ByteString, Message, ProtocolMessageEnum} +import com.google.protobuf.{ByteString, MapEntry, Message, ProtocolMessageEnum} import magnolia1.* import magnolify.shared.* import magnolify.shims.FactoryCompat @@ -54,17 +54,13 @@ object ProtobufType { r.checkDefaults(descriptor)(cm) } - @transient private var _newBuilder: Method = _ - private def newBuilder: Message.Builder = { - if (_newBuilder == null) { - _newBuilder = ct.runtimeClass.getMethod("newBuilder") - } + @transient private lazy val _newBuilder: Method = ct.runtimeClass.getMethod("newBuilder") + private def newBuilder(): Message.Builder = _newBuilder.invoke(null).asInstanceOf[Message.Builder] - } private val caseMapper: CaseMapper = cm override def from(v: MsgT): T = r.from(v)(caseMapper) - override def to(v: T): MsgT = r.to(v, newBuilder)(caseMapper).asInstanceOf[MsgT] + override def to(v: T): MsgT = r.to(v, newBuilder())(caseMapper).asInstanceOf[MsgT] } case _ => throw new IllegalArgumentException(s"ProtobufType can only be created from Record. Got $f") @@ -130,6 +126,10 @@ object ProtobufField { } ) + private def newFieldBuilder(b: Message.Builder)(f: FieldDescriptor): Message.Builder = + if (f.getType != FieldDescriptor.Type.MESSAGE) null + else b.newBuilderForField(f) + override def checkDefaults(descriptor: Descriptor)(cm: CaseMapper): Unit = { val fields = getFields(descriptor)(cm) caseClass.parameters.foreach { p => @@ -169,17 +169,11 @@ object ProtobufField { override def to(v: T, bu: Message.Builder)(cm: CaseMapper): Message = { val fields = getFields(bu.getDescriptorForType)(cm) - caseClass.parameters .foldLeft(bu) { (b, p) => val field = fields(p.index) - val value = if (field.getType == FieldDescriptor.Type.MESSAGE) { - // nested records - p.typeclass.to(p.dereference(v), b.newBuilderForField(field))(cm) - } else { - // non-nested - p.typeclass.to(p.dereference(v), null)(cm) - } + val builder = newFieldBuilder(bu)(field) + val value = p.typeclass.to(p.dereference(v), builder)(cm) if (value == null) b else b.setField(field, value) } .build() @@ -284,4 +278,44 @@ object ProtobufField { override def to(v: C[T], b: Message.Builder)(cm: CaseMapper): ju.List[f.ToT] = if (v.isEmpty) null else v.iterator.map(f.to(_, b)(cm)).toList.asJava } + + implicit def pfMap[K, V](implicit + kf: ProtobufField[K], + vf: ProtobufField[V] + ): ProtobufField[Map[K, V]] = + new Aux[Map[K, V], ju.List[MapEntry[kf.FromT, vf.FromT]], ju.List[MapEntry[kf.ToT, vf.ToT]]] { + + override val default: Option[Map[K, V]] = Some(Map.empty) + + override def from(v: ju.List[MapEntry[kf.FromT, vf.FromT]])(cm: CaseMapper): Map[K, V] = { + val b = Map.newBuilder[K, V] + if (v != null) { + b ++= v.asScala.map(me => kf.from(me.getKey)(cm) -> vf.from(me.getValue)(cm)) + } + b.result() + } + + private def newFieldBuilder(b: Message.Builder)(f: FieldDescriptor): Message.Builder = + if (f.getType != FieldDescriptor.Type.MESSAGE) null + else b.newBuilderForField(f) + + override def to(v: Map[K, V], b: Message.Builder)( + cm: CaseMapper + ): ju.List[MapEntry[kf.ToT, vf.ToT]] = { + if (v.isEmpty) { + null + } else { + val keyField = b.getDescriptorForType.findFieldByName("key") + val valueField = b.getDescriptorForType.findFieldByName("value") + v.map { case (k, v) => + b + .setField(keyField, kf.to(k, newFieldBuilder(b)(keyField))(cm)) + .setField(valueField, vf.to(v, newFieldBuilder(b)(valueField))(cm)) + .build() + .asInstanceOf[MapEntry[kf.ToT, vf.ToT]] + }.toList + .asJava + } + } + } } diff --git a/protobuf/src/test/protobuf/Proto2.proto b/protobuf/src/test/protobuf/Proto2.proto index af4f12d1e..ab41afa72 100644 --- a/protobuf/src/test/protobuf/Proto2.proto +++ b/protobuf/src/test/protobuf/Proto2.proto @@ -41,19 +41,24 @@ message NestedP2 { repeated RequiredP2 l = 6; } -message CollectionP2 { +message CollectionsP2 { repeated int32 a = 1; repeated int32 l = 2; repeated int32 v = 3; repeated int32 s = 4; } -message MoreCollectionP2 { +message MoreCollectionsP2 { repeated int32 i = 1; repeated int32 s = 2; repeated int32 is = 3; } +message MapsP2 { + map mp = 1; + map mn = 2; +} + message EnumsP2 { enum JavaEnums { RED = 0; diff --git a/protobuf/src/test/protobuf/Proto3.proto b/protobuf/src/test/protobuf/Proto3.proto index 1fe02fc6f..e9a9c23ff 100644 --- a/protobuf/src/test/protobuf/Proto3.proto +++ b/protobuf/src/test/protobuf/Proto3.proto @@ -41,19 +41,24 @@ message NestedP3 { repeated RequiredP3 l = 6; } -message CollectionP3 { +message CollectionsP3 { repeated int32 a = 1; repeated int32 l = 2; repeated int32 v = 3; repeated int32 s = 4; } -message MoreCollectionP3 { +message MoreCollectionsP3 { repeated int32 i = 1; repeated int32 s = 2; repeated int32 is = 3; } +message MapsP3 { + map mp = 1; + map mn = 2; +} + message EnumsP3 { enum JavaEnums { RED = 0; diff --git a/protobuf/src/test/scala/magnolify/protobuf/ProtobufTypeSuite.scala b/protobuf/src/test/scala/magnolify/protobuf/ProtobufTypeSuite.scala index c6cb11637..c7bfd7423 100644 --- a/protobuf/src/test/scala/magnolify/protobuf/ProtobufTypeSuite.scala +++ b/protobuf/src/test/scala/magnolify/protobuf/ProtobufTypeSuite.scala @@ -72,10 +72,13 @@ class ProtobufTypeSuite extends BaseProtobufTypeSuite { test[UnsafeChar, IntegersP2] test[UnsafeShort, IntegersP2] - test[Collections, CollectionP2] - test[Collections, CollectionP3] - test[MoreCollections, MoreCollectionP2] - test[MoreCollections, MoreCollectionP3] + test[Collections, CollectionsP2] + test[Collections, CollectionsP3] + test[MoreCollections, MoreCollectionsP2] + test[MoreCollections, MoreCollectionsP3] + + test[Maps, MapsP2] + test[Maps, MapsP3] test("AnyVal") { test[ProtoHasValueClass, IntegersP2] @@ -165,6 +168,7 @@ object Proto3Enums { ProtobufField.enum[ADT.Color, EnumsP3.ScalaEnums] } +case class Maps(mp: Map[String, Int], mn: Map[String, Nested]) case class ProtoValueClass(value: Long) extends AnyVal case class ProtoHasValueClass(i: Int, l: ProtoValueClass) case class UnsafeByte(i: Byte, l: Long) diff --git a/test/src/test/scala/magnolify/test/Simple.scala b/test/src/test/scala/magnolify/test/Simple.scala index b9b03eeef..a08902ecb 100644 --- a/test/src/test/scala/magnolify/test/Simple.scala +++ b/test/src/test/scala/magnolify/test/Simple.scala @@ -51,6 +51,7 @@ object Simple { o: Option[Required], l: List[Required] ) + case class Collections(a: Array[Int], l: List[Int], v: Vector[Int], s: Set[Int]) { override def hashCode(): Int = { @@ -71,7 +72,12 @@ object Simple { case _ => false } } - case class MoreCollections(i: Iterable[Int], s: Seq[Int], is: IndexedSeq[Int]) + case class MoreCollections( + i: Iterable[Int], + s: Seq[Int], + is: IndexedSeq[Int] + ) + case class Enums( j: JavaEnums.Color, s: ScalaEnums.Color.Type, @@ -94,6 +100,7 @@ object Simple { sr: List[UnsafeEnum[ScalaEnums.Color.Type]], ar: List[UnsafeEnum[ADT.Color]] ) + case class Custom(u: URI, d: Duration) case class LowerCamel(firstField: String, secondField: String, innerField: LowerCamelInner)