class: center, middle # Chimney YannMoisan --- # What is it ```scala A => B ``` --- # Any questions --- # Example 1 ```scala // --- From --- case class Amount(currency: String, value: BigDecimal) case class Transaction( id: String, executionDate: String, amount: Amount ) // --- To --- case class TransactionRow( id: String, executionAt: String, amountCurrency: String, amountValue: BigDecimal, status: String ) ``` --- ```scala transaction.into[TransactionRow].transform ``` ```console [error] 23 | val row = transaction.into[TransactionRow].transform [error] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ [error] |Chimney can't derive transformation from talks.Transaction to talks.TransactionRow [error] | [error] |talks.TransactionRow [error] | executionAt: java.lang.String - no accessor named executionAt in source type talks.Transaction [error] | amountCurrency: java.lang.String - no accessor named amountCurrency in source type talks.Transaction [error] | amountValueCurrency: scala.math.BigDecimal - no accessor named amountValueCurrency in source type talks.Transaction [error] | status: java.lang.String - no accessor named status in source type talks.Transaction [error] | [error] |Consult https://chimney.readthedocs.io for usage examples. [error] | ``` --- 🔴 ```scala .withFieldConst(_.executionAt, transaction.executionDate)` ``` --- 🔴 ```scala .withFieldConst(_.executionAt, transaction.executionDate)` ``` 🔴 ```scala .withFieldComputed(_.executionAt, _.executionDate) ``` --- 🔴 ```scala .withFieldConst(_.executionAt, transaction.executionDate)` ``` 🔴 ```scala .withFieldComputed(_.executionAt, _.executionDate) ``` ✅ ```scala .withFieldRenamed(_.executionDate, _.executionAt) ``` --- # Also works with nested ✅ ```scala .withFieldRenamed(_.amount.currency, _.amountCurrency) .withFieldRenamed(_.amount.value, _.amountValue) ``` --- ```scala val row = transaction .into[TransactionRow] .withFieldRenamed(_.executionDate, _.executionAt) .withFieldRenamed(_.amount.currency, _.amountCurrency) .withFieldRenamed(_.amount.value, _.amountValue) .withFieldConst(_.status, "Booked") .transform ``` --- # Example 2 ```scala // From case class From(from: String) // To case class Reference(value: String) case class To(to: Reference) ``` --- 🔴 ```scala .withFieldComputed(_.to, from => Reference(from.from)) ``` --- 🔴 ```scala .withFieldComputed(_.to, from => Reference(from.from)) ``` ✅ ```scala .withFieldComputedFrom(_.from)(_.to, Reference(_)) ``` --- 🔴 ```scala .withFieldComputed(_.to, from => Reference(from.from)) ``` ✅ ```scala .withFieldComputedFrom(_.from)(_.to, Reference(_)) ``` ✅✅ ```scala implicit val stringToReferenceTransformer: Transformer[String, Reference] = (src: String) => Reference(src) ... .withFieldRenamed(_.from, _.to) ``` --- # 2 types of transformation - Total: `A => B` (every A can be converted to B) - Partial: `A => Result[B]` (not all A can be converted to B) --- # Real-world example - DB - Domain => Row (total) - Row => Domain (partial) - Protobuf - Domain => Protobuf (total) - Protobuf => Domain (partial) --- # Example 3 ```scala syntax = "proto3"; import "google/protobuf/timestamp.proto"; message Transaction { string id = 1; google.protobuf.Timestamp created_at = 2; google.protobuf.Timestamp updated_at = 3; Status status = 4; string amount = 5; string currency = 6; enum Status { STATUS_ACCEPTED = 0; STATUS_EXECUTED = 1; STATUS_REJECTED = 2; } } ``` --- ```scala case class Transaction( id: String, createdAt: Instant, updatedAt: Instant, status: Status, amount: BigDecimal, currency: Currency ) enum Status { case Accepted case Executed case Rejected } enum Currency { case EUR case USD } ``` --- # Vanilla ```scala def fromProto(proto: transaction.Transaction): Either[String, Transaction] = for { status <- fromProto(proto.status).toRight("unrecognized status") amount <- toBigDecimal(proto.amount) createdAt <- proto.createdAt.map(toInstant).toRight("empty timestamp") updatedAt <- proto.updatedAt.map(toInstant).toRight("empty timestamp") currency <- toCurrency(proto.currency) } yield Transaction( id = proto.id, createdAt = createdAt, updatedAt = updatedAt, status = status, amount = amount, currency = currency ) ``` --- # Vanilla ```scala def fromProto(status: transaction.Transaction.Status): Option[Status] = { status match { case transaction.Transaction.Status.Unrecognized(_) => None case transaction.Transaction.Status.STATUS_ACCEPTED => Some(Status.Accepted) case transaction.Transaction.Status.STATUS_EXECUTED => Some(Status.Executed) case transaction.Transaction.Status.STATUS_REJECTED => Some(Status.Rejected) } } private def toInstant(timestamp: Timestamp) = Instant.ofEpochSecond(timestamp.seconds, timestamp.nanos.toLong) private def toBigDecimal(s: String): Either[String, BigDecimal] = { Try(BigDecimal(s)).toEither.left.map(_ => s"'$s' is not a number") } private def toCurrency(s: String) = Try(Currency.valueOf(s)).toEither.left.map(_ => s"'$s' is not a currency") ``` --- # Chimney ```scala implicit val stringToBigDecimal: PartialTransformer[String, BigDecimal] = PartialTransformer.fromFunction(BigDecimal(_)) ``` --- # Chimney ```scala implicit val stringToCurrency: PartialTransformer[String, Currency] = PartialTransformer.fromFunction(Currency.valueOf) ``` --- # Chimney ```scala implicit val statusToStatus: PartialTransformer[transaction.Transaction.Status, Status] = PartialTransformer .define[transaction.Transaction.Status, Status] .withSealedSubtypeRenamed[ transaction.Transaction.Status.STATUS_ACCEPTED.type, Status.Accepted.type ] .withSealedSubtypeRenamed[ transaction.Transaction.Status.STATUS_EXECUTED.type, Status.Executed.type ] .withSealedSubtypeRenamed[ transaction.Transaction.Status.STATUS_REJECTED.type, Status.Rejected.type ] .buildTransformer ``` --- # Chimney ```scala proto.intoPartial[Transaction].transform ``` --- # Chimney - Bonus ```scala sealed abstract class IgnorePrefixNamesComparison(prefix: String) extends TransformedNamesComparison { this: Singleton => def namesMatch(fromName: String, toName: String): Boolean = fromName == prefix + toName } case object StatusNamesComparison extends IgnorePrefixNamesComparison("Status") .enableCustomSubtypeNameComparison(StatusNamesComparison) ``` --- # Benefits 1. the field name is automatically included in the error to ease debugging (no risk of inconsistency) 2. all errors are accumulated 3. no need to use for comprehension, Chimney handles the combination of errors for us 4. if you have a `PartialTransformer[A, B]`, you get a `PartialTransformer[Option[A], B]` that will return an error if `Option[A]` is empty for free. This is useful for timestamps that are wrapped in an Option and more generally all objects are optional in proto3. See: [frominto-an-option](https://chimney.readthedocs.io/en/stable/supported-transformations/#frominto-an-option) --- # Protobuf support - a Transformer instance for `com.google.protobuf.timestamp.Timestamp` - automatic handling of `Unrecognized` case for enums. See: [enum-fields](https://chimney.readthedocs.io/en/stable/cookbook/#enum-fields) --- # Limits 1: multiple from ## Example When the source involved more than one object. ```scala // From case class FromA(a1: String, a2: String) case class FromB(b1: String, b2: String) // To case class To(a1: String, a2: String, b1: String, b2: String) ``` --- # Limits 1: multiple from ## choose the one with the more fields to map into the dest ```scala fromA .into[To] .withFieldConst(_.b1, fromB.b1) .withFieldConst(_.b2, fromB.b2) .transform ``` --- # Limits 1 ## work on tuple ```scala (fromA, fromB) .into[To] .withFieldRenamed(_._1.a1, _.a1) .withFieldRenamed(_._1.a2, _.a2) .withFieldRenamed(_._2.b1, _.b1) .withFieldRenamed(_._2.b2, _.b2) .transform ``` --- # Thanks @nrinaudo for the inspiration [scala-best-practices](https://nrinaudo.github.io/talk-scala-best-practices/)