ducktape 0.2.0-M2
ducktape 0.2.0-M2
The 0.2.x series comes with a lot of improvements, some of them being:
- support for configurations for nested transformations (previously these would need to be their own
Transformer
instance, no matter how basic the config was), - the library now reports all of the transformation errors all at once (with copy-pastable hints on how to fix a given error, eg.
No field 'name' found in MdocApp0.this.wire.PaymentMethod.Card @ Person.paymentMethods.element.at[MdocApp0.this.domain.Payment.Card].name
...and many others.
The 0.2.x series also retains source compatibility with the 0.1.x
series to the fullest extent possible (there are SOME changes which will be documented in detail some time in the future) - but generally your code should compile and work as is.
This milestone release only contains support for total
transformations, fallible
transformations are coming soon.
Read on over to see what is actually possible now or read the docs and try it for yourself:
import java.time.Instant
import io.github.arainko.ducktape.*
// imagine this is a wire model of some kind - JSON, protobuf, avro, what have you...
object wire {
final case class Person(
firstName: String,
lastName: String,
paymentMethods: List[wire.PaymentMethod],
status: wire.Status,
updatedAt: Option[Instant],
)
enum Status:
case Registered, PendingRegistration, Removed
enum PaymentMethod:
case Card(name: String, digits: Long, expires: Instant)
case PayPal(email: String)
case Cash
}
object domain {
final case class Person( // <-- fields reshuffled
lastName: String,
firstName: String,
status: Option[domain.Status], // <-- 'status' in the domain model is optional
paymentMethods: Vector[domain.Payment], // <-- collection type changed from a List to a Vector
updatedAt: Option[Instant],
)
enum Status:
case Registered, PendingRegistration, Removed
case PendingRemoval // <-- additional enum case
enum Payment:
case Card(name: String, digits: Long, expires: Instant)
case PayPal(email: String)
case Cash
}
val wirePerson: wire.Person = wire.Person(
"John",
"Doe",
List(
wire.PaymentMethod.Cash,
wire.PaymentMethod.PayPal("john@doe.com"),
wire.PaymentMethod.Card("J. Doe", 12345, Instant.now)
),
wire.Status.PendingRegistration,
Some(Instant.ofEpochSecond(0))
)
val domainPerson = wirePerson.to[domain.Person]
// domainPerson: Person = Person(
// lastName = "Doe",
// firstName = "John",
// status = Some(value = PendingRegistration),
// paymentMethods = Vector(
// Cash,
// PayPal(email = "john@doe.com"),
// Card(
// name = "J. Doe",
// digits = 12345L,
// expires = 2023-12-17T14:54:13.882678035Z
// )
// ),
// updatedAt = Some(value = 1970-01-01T00:00:00Z)
// )
Click to see the generated code
(({
val paymentMethods$2: Vector[Payment] = MdocApp.this.wirePerson.paymentMethods
.map[Payment]((src: PaymentMethod) =>
if (src.isInstanceOf[Card])
new Card(
name = src.asInstanceOf[Card].name,
digits = src.asInstanceOf[Card].digits,
expires = src.asInstanceOf[Card].expires
)
else if (src.isInstanceOf[PayPal]) new PayPal(email = src.asInstanceOf[PayPal].email)
else if (src.isInstanceOf[Cash.type]) MdocApp.this.domain.Payment.Cash
else throw new RuntimeException("Unhandled condition encountered during Coproduct Transformer derivation")
)
.to[Vector[Payment]](iterableFactory[Payment])
val status$2: Some[Status] = Some.apply[Status](
if (MdocApp.this.wirePerson.status.isInstanceOf[Registered.type]) MdocApp.this.domain.Status.Registered
else if (MdocApp.this.wirePerson.status.isInstanceOf[PendingRegistration.type])
MdocApp.this.domain.Status.PendingRegistration
else if (MdocApp.this.wirePerson.status.isInstanceOf[Removed.type]) MdocApp.this.domain.Status.Removed
else throw new RuntimeException("Unhandled condition encountered during Coproduct Transformer derivation")
)
new Person(
lastName = MdocApp.this.wirePerson.lastName,
firstName = MdocApp.this.wirePerson.firstName,
status = status$2,
paymentMethods = paymentMethods$2,
updatedAt = MdocApp.this.wirePerson.updatedAt
)
}: Person): Person)
But now imagine that your wire model differs ever so slightly from your domain model, maybe the wire model's PaymentMethod.Card
doesn't have the name
field for some inexplicable reason...
object wire {
final case class Person(
firstName: String,
lastName: String,
paymentMethods: List[wire.PaymentMethod],
status: wire.Status,
updatedAt: Option[Instant],
)
enum Status:
case Registered, PendingRegistration, Removed
enum PaymentMethod:
case Card(digits: Long, expires: Instant) // <-- poof, 'name' is gone
case PayPal(email: String)
case Cash
}
val wirePerson: wire.Person = wire.Person(
"John",
"Doe",
List(
wire.PaymentMethod.Cash,
wire.PaymentMethod.PayPal("john@doe.com"),
wire.PaymentMethod.Card(12345, Instant.now)
),
wire.Status.PendingRegistration,
Some(Instant.ofEpochSecond(0))
)
...and when you try to transform between these two representations the compiler now yells at you.
val domainPerson = wirePerson.to[domain.Person]
// error:
// No field 'name' found in MdocApp0.this.wire.PaymentMethod.Card @ Person.paymentMethods.element.at[MdocApp0.this.domain.Payment.Card].name
// case Cash
// ^
Now onto dealing with that, let's first examine the error message:
No field 'name' found in MdocApp0.this.wire.PaymentMethod.Card @ Person.paymentMethods.element.at[MdocApp0.this.domain.Payment.Card].name
especially the part after @
:
Person.paymentMethods.element.at[MdocApp0.this.domain.Payment.Card].name
the thing above is basically a path to the field/subtype under which ducktape
was not able to create a transformation, these are meant to be copy-pastable for when you're actually trying to fix the error, eg. by setting the name
field to a constant value:
val domainPerson =
wirePerson
.into[domain.Person]
.transform(Field.const(_.paymentMethods.element.at[domain.Payment.Card].name, "CONST NAME"))
// domainPerson: Person = Person(
// lastName = "Doe",
// firstName = "John",
// status = Some(value = PendingRegistration),
// paymentMethods = Vector(
// Cash,
// PayPal(email = "john@doe.com"),
// Card(
// name = "CONST NAME",
// digits = 12345L,
// expires = 2023-12-17T14:54:13.890982787Z
// )
// ),
// updatedAt = Some(value = 1970-01-01T00:00:00Z)
// )
Click to see the generated code
{
val AppliedBuilder_this: AppliedBuilder[Person, Person] = into[Person](MdocApp1.this.wirePerson1)[MdocApp1.this.domain.Person]
{
val value$proxy3: Person = AppliedBuilder_this.inline$value
{
val paymentMethods$4: Vector[Payment] = value$proxy3.paymentMethods
.map[Payment]((src: PaymentMethod) =>
if (src.isInstanceOf[Card])
new Card(name = "CONST NAME", digits = src.asInstanceOf[Card].digits, expires = src.asInstanceOf[Card].expires)
else if (src.isInstanceOf[PayPal]) new PayPal(email = src.asInstanceOf[PayPal].email)
else if (src.isInstanceOf[Cash.type]) MdocApp1.this.domain.Payment.Cash
else throw new RuntimeException("Unhandled condition encountered during Coproduct Transformer derivation")
)
.to[Vector[Payment]](iterableFactory[Payment])
val status$4: Some[Status] = Some.apply[Status](
if (value$proxy3.status.isInstanceOf[Registered.type]) MdocApp1.this.domain.Status.Registered
else if (value$proxy3.status.isInstanceOf[PendingRegistration.type]) MdocApp1.this.domain.Status.PendingRegistration
else if (value$proxy3.status.isInstanceOf[Removed.type]) MdocApp1.this.domain.Status.Removed
else throw new RuntimeException("Unhandled condition encountered during Coproduct Transformer derivation")
)
new Person(
lastName = value$proxy3.lastName,
firstName = value$proxy3.firstName,
status = status$4,
paymentMethods = paymentMethods$4,
updatedAt = value$proxy3.updatedAt
)
}: Person
}: Person
}