Supported Patching
Besides transforming Chimney is also able to perform patching - take a "patched" value, a "patch" value, and compute the updated version of the patched value.
Updating case class
Currently, the only supported case is updating one case class
with another:
Example
//> using dep io.scalaland::chimney::1.5.0
//> using dep com.lihaoyi::pprint::0.9.0
import io.scalaland.chimney.dsl._
case class Email(address: String) extends AnyVal
case class Phone(number: Long) extends AnyVal
case class User(id: Int, email: Email, phone: Phone)
case class UserUpdateForm(email: String, phone: Long)
val user = User(10, Email("abc@@domain.com"), Phone(1234567890L))
val updateForm = UserUpdateForm("xyz@@domain.com", 123123123L)
pprint.pprintln(
user.patchUsing(updateForm)
)
// expected output:
// User(id = 10, email = Email(address = "xyz@@domain.com"), phone = Phone(number = 123123123L))
As we see the values from the "patch" aren't always of the same type as the values they are supposed to update.
In such case, macros use Transformer
s logic under the hood to convert a patch into a patched value.
Notice
Currently Patcher
s are flat - they cannot perform a nested update.
Ignoring fields in patches
When the patch case class
contains a field that does not exist in patched object, Chimney will not be able to generate
Patcher
.
Example
//> using dep io.scalaland::chimney::1.5.0
import io.scalaland.chimney.dsl._
case class User(id: Int, email: String, phone: Long)
case class UserUpdateForm(email: String, phone: Long, address: String)
val user = User(10, "abc@@domain.com", 1234567890L)
user.patchUsing(UserUpdateForm("xyz@@domain.com", 123123123L, "some address"))
// expected error:
// Chimney can't derive patching for User with patch type UserUpdateForm
//
// Field named 'address' not found in target patching type snippet.User!
//
// Consult https://chimney.readthedocs.io for usage examples.
This default behavior is intentional to prevent silent oversight of typos in patcher field names.
But there is a way to ignore redundant patcher fields explicitly with .ignoreRedundantPatcherFields
operation.
Example
//> using dep io.scalaland::chimney::1.5.0
//> using dep com.lihaoyi::pprint::0.9.0
import io.scalaland.chimney.dsl._
case class User(id: Int, email: String, phone: Long)
case class UserUpdateForm(email: String, phone: Long, address: String)
val user = User(10, "abc@@domain.com", 1234567890L)
pprint.pprintln(
user
.using(UserUpdateForm("xyz@@domain.com", 123123123L, "some address"))
.ignoreRedundantPatcherFields
.patch
)
// expected output:
// User(id = 10, email = "xyz@@domain.com", phone = 123123123L)
locally {
// All patching derived in this scope will see these new flags (Scala 2-only syntax, see cookbook for Scala 3)
implicit val cfg = PatcherConfiguration.default.ignoreRedundantPatcherFields
pprint.pprintln(
user.patchUsing(UserUpdateForm("xyz@@domain.com", 123123123L, "some address"))
)
// expected output:
// User(id = 10, email = "xyz@@domain.com", phone = 123123123L)
}
Patching succeeded using only relevant fields that appear in the patched object and ignoring address: String
field
from the patch.
If the flag was enabled in the implicit config it can be disabled with .failRedundantPatcherFields
.
Example
//> using dep io.scalaland::chimney::1.5.0
import io.scalaland.chimney.dsl._
case class User(id: Int, email: String, phone: Long)
case class UserUpdateForm(email: String, phone: Long, address: String)
val user = User(10, "abc@@domain.com", 1234567890L)
// All patching derived in this scope will see these new flags (Scala 2-only syntax, see cookbook for Scala 3)
implicit val cfg = PatcherConfiguration.default.ignoreRedundantPatcherFields
user
.using(UserUpdateForm("xyz@@domain.com", 123123123L, "some address"))
.failRedundantPatcherFields
.patch
// expected error:
// Chimney can't derive patching for User with patch type UserUpdateForm
//
// Field named 'address' not found in target patching type User!
//
// Consult https://chimney.readthedocs.io for usage examples.
Treating None
as no-update instead of "set to None
"
It is possible to patch using optional values of type Option[A]
as long as the Transformer
is available for A
.
If the value is present (Some
), it’s used for patching a field in the target object; otherwise (None
) it’s ignored
and the field value is copied from the original object.
Let’s consider the following patch:
Example
//> using dep io.scalaland::chimney::1.5.0
//> using dep com.lihaoyi::pprint::0.9.0
import io.scalaland.chimney.dsl._
case class User(id: Int, email: String, phone: Long)
case class UserPatch(email: Option[String], phone: Option[Long])
val user = User(10, "abc@@domain.com", 1234567890L)
val update = UserPatch(email = Some("updated@@example.com"), phone = None)
pprint.pprintln(
user.patchUsing(update)
)
// expected output:
// User(id = 10, email = "updated@@example.com", phone = 1234567890L)
The field phone
remained the same as in the original user
, while the optional e-mail string got updated from
a patch object.
Option[A]
on both sides
An interesting case appears when both the patch case class
and the patched object define fields f: Option[A]
Depending on the values of f
in the patched object and patch, we would like to apply the following semantic table:
patchedObject.f |
patch.f |
patching result |
---|---|---|
None |
Some(value) |
Some(value) |
Some(value1) |
Some(value2) |
Some(value2) |
None |
None |
None |
Some(value) |
None |
??? |
When a patch.f
contains some value, it’s immediately used for replacing a field in the target object (rows 1 and 2),
regardless of the original object field value. When both field are None
, the patching result is also None
(row 3).
But if the original object contains a some value, but the patch comes with a None
, we can do two things:
- clear value in target object (replace it with None)
- or ignore updating this particular field (as in the previous section)
Both choices may make perfect sense, depending on the context. By default, Chimney does the former (clears the value),
but it also gives a simple way to always ignore None
from the patch with .ignoreNoneInPatch
operation.
Example
//> using dep io.scalaland::chimney::1.5.0
//> using dep com.lihaoyi::pprint::0.9.0
import io.scalaland.chimney.dsl._
case class User(name: Option[String], age: Option[Int])
case class UserPatch(name: Option[String], age: Option[Int])
val user = User(Some("John"), Some(30))
val userPatch = UserPatch(None, None)
pprint.pprintln(
user.patchUsing(userPatch)
)
// clears both fields:
// expected output:
// User(name = None, age = None)
pprint.pprintln(
user
.using(userPatch)
.ignoreNoneInPatch
.patch
)
// ignores updating both fields:
// expected output:
// User(name = Some(value = "John"), age = Some(value = 30))
locally {
// All patching derived in this scope will see these new flags (Scala 2-only syntax, see cookbook for Scala 3)
implicit val cfg = PatcherConfiguration.default.ignoreNoneInPatch
pprint.pprintln(
user.patchUsing(userPatch)
)
// ignores updating both fields:
// expected output:
// User(name = Some(value = "John"), age = Some(value = 30))
}
If the flag was enabled in the implicit config it can be disabled with .clearOnNoneInPatch
.
Example
//> using dep io.scalaland::chimney::1.5.0
//> using dep com.lihaoyi::pprint::0.9.0
import io.scalaland.chimney.dsl._
case class User(name: Option[String], age: Option[Int])
case class UserPatch(name: Option[String], age: Option[Int])
val user = User(Some("John"), Some(30))
val userPatch = UserPatch(None, None)
// all patching derived in this scope will see these new flags
implicit val cfg = PatcherConfiguration.default.ignoreNoneInPatch
pprint.pprintln(
user
.using(userPatch)
.clearOnNoneInPatch
.patch
)
// clears both fields:
// expected output:
// User(name = None, age = None)