scalalandio / chimney

Scala library for boilerplate-free, type-safe data transformations
https://chimney.readthedocs.io
Apache License 2.0
1.15k stars 91 forks source link

Bidirectional mapping #96

Closed lloydmeta closed 2 months ago

lloydmeta commented 5 years ago

Sometimes if you have two classes, it might be nice to be able to define a single bi-directional transform instead of having to write one for each one

// To send to your fancy no-sql store; fixed Schema
case class PersistedUser(name: String, address: String) 

// To send to your users
case class ApiUser(name: String, streetAddress: String)

// Declare a 2 way transformer
val biTransformer = BiTransform[PersistedUser, ApiUser]
  .withFieldRenamed(_.address, _.streetAdress)

val p = PersistedUser("Coookie", "Sesame Street")
val a = biTransformer.from(p)
val pAgain = biTransformer.from(a)

Not sure if/how this could be done, but just a thought that was inspired somewhat by play-json's `Format, which acts as both an encoder and a decoder, coupled to their functional combinator syntax, which comes together to allow for this sort of thing:

// Bi-directional formatter with renaming.
val locationFormat: Format[Location] = (
      (JsPath \ "lat").format[Double] and
      (JsPath \ "long").format[Double]
    )(Location.apply, unlift(Location.unapply))
Actual Ammonite-usage of play-json ```scala 14:00 $ amm Loading... Welcome to the Ammonite Repl 1.0.5 (Scala 2.12.4 Java 1.8.0_152) If you like Ammonite, please support our development at www.patreon.com/lihaoyi @ import $ivy.{`com.typesafe.play::play-json:2.6.10`} import $ivy.$ @ import play.api.libs.json._, play.api.libs.functional.syntax._ import play.api.libs.json._, play.api.libs.functional.syntax._ @ case class Location(latitude: Double, longitude: Double) defined class Location @ implicit val locationFormat: Format[Location] = ( (JsPath \ "lat").format[Double] and (JsPath \ "long").format[Double] )(Location.apply, unlift(Location.unapply)) locationFormat: Format[Location] = play.api.libs.json.OFormat$$anon$1@519e67e @ val l = Location(123d, 123d) l: Location = Location(123.0, 123.0) @ val j = Json.toJson(l) j: JsValue = JsObject(Map("lat" -> JsNumber(123.0), "long" -> JsNumber(123.0))) @ val lAgain = j.as[Location] lAgain: Location = Location(123.0, 123.0) ```

It seems to me like doing something like that in Chimney would be "even better" because we don't need to mess around with a JsPath and things are fully typed.

yoohaemin commented 5 years ago

Maybe you can utilize isomorphisms from other libraries (like monocle.Iso), and define two implicit Transformer instances based on that?

ghostdogpr commented 4 years ago

Also interested in this kind of feature. When you need transformation both ways, it would be nice to be able to define the field mapping only once.

MateuszKubuszok commented 10 months ago

If anyone is interested in implementing such thing, it would require:

All in all, it is not a difficult work, so most new contributors should be able to do it, but it is also not a one liner to implement during afternoon.