Bulletin is a simple but useful library for merging updates into case classes. You create two classes, one representing a data record and one a partial update. Bulletin type-checks the classes and automatically generates a function to merge them.
Suppose we’re writing an address book application that models our friends as instances of a case class Person
. In addition to Person
we define a case class PersonUpdate
representing a partial update:
case class Person(
id: Long,
name: String,
email: Option[String])
case class PersonUpdate(
name: Option[String],
email: Option[Option[String]])
val person = Person(123L, "Bruce Wayne", Some("bruce@waynemanor.com"))
val update = Update(Some("Batman"), Some(None))
With these two case classes in place, we can mechanically define a merge
function by matching the field names and writing calls to getOrElse
:
def mergePerson(person: Person, update: PersonUpdate): Person =
person.copy(
name = update.name getOrElse person.name,
email = update.email getOrElse person.email
)
Writing merge functions is tedious and violates the principle of DRY (“Don’t Repeat Yourself”). Bulletin avoids this problem by providing a single merge
operation that works for all compatible case classes:
import bulletin._
val updated = person merge update
// updated: Person = Person(123L, "Batman", None)
How does it work? Bulletin uses a type class called Merge
behind the scenes to ensure it can apply an update to a record:
trait Merge[A, B] {
def apply(record: A, update: B): A
}
def merge[A, B](record: A, update: B)(implicit m: Merge[A, B]): A =
m(record, update)
Bulletin uses shapeless to automatically define Merge
instances for pairs of case classes. It requires that every field in the update class has a corresponding field of matching type in the record class. Otherwise you get a compile error:
person merge "This isn't an update!"
// compile error:
// Cannot update a Person with a String.
// Check the field types match up,
// or manually create a Merge instance for these types.
If your data doesn’t form a neat pair of case classes, you can write a Merge
instance by hand. It’s less convenient but it maintains the same interface for all merges:
implicit val pairMerge[A, B] = new Merge[(A, B), (Option[A], Option[B])] {
def apply(record: (A, B), update: (Option[A], Option[B])): (A, B) = (
update._1 getOrElse record._1,
update._2 getOrElse record._2
)
}
val updated = (123, 234) merge (Option.empty[Int], Option(345))
// updated: (Int, Int) = (123, 345)
You can grab Bulletin via SBT. Check the README on GitHub for details.