Last active
March 13, 2022 15:42
-
-
Save calvinlfer/cb38523e4536ecda1b6cf14ee3b03ff5 to your computer and use it in GitHub Desktop.
This shows how to do a Diff + Merge for any datatype provided you have the appropriate Diff and Merge all the primitive instances used by the datatype. This is done using Shapeless 2.3.3. Diff and Merge go hand-in-hand
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Unhappy with the default behavior of a particular datatype? | |
// You can override it as you wish, you must write a Diff and merge for the datatype you wish to override | |
final case class Address( | |
streetNumber: Int, | |
streetName: String, | |
city: String, | |
country: String | |
) | |
// We want to override Address in a way that we the user deem fit | |
final case class AddressDiff( | |
streetNumber: Diff[Int], | |
streetName: Diff[String], | |
city: Diff[String], | |
country: Diff[String] | |
) | |
implicit val deltaOptAddress: Delta.Aux[Option[Address], Option[AddressDiff]] = | |
new Delta[Option[Address]] { | |
override type Out = Option[AddressDiff] | |
override def apply(existing: Option[Address], incoming: Option[Address]): Out = | |
(existing, incoming) match { | |
case (Some(Address(eSNum, eSNam, eCity, eCountry)), Some(Address(iSNum, iSNam, iCity, iCountry))) => | |
Option( | |
AddressDiff( | |
intDelta(eSNum, iSNum), | |
stringDelta(eSNam, iSNam), | |
stringDelta(eCity, iCity), | |
stringDelta( | |
eCountry, | |
iCountry | |
) | |
) | |
) | |
case (None, Some(Address(iSNum, iSNam, iCity, iCountry))) => | |
Option( | |
AddressDiff(Diff.Change(iSNum), Diff.Change(iSNam), Diff.Change(iCity), Diff.Change(iCountry)) | |
) | |
case (Some(Address(_, _, _, _)), None) => | |
None | |
case (None, None) => None | |
} | |
} | |
implicit val mergeOptAddress: Merge.Aux[Option[Address], Option[AddressDiff]] = | |
new Merge[Option[Address]] { | |
override type D = Option[AddressDiff] | |
override def apply(base: Option[Address], diff: D): Option[Address] = (base, diff) match { | |
case (Some(existing), Some(diff)) => | |
Some( | |
existing | |
.copy( | |
streetNumber = diff.streetNumber.getOrElse(existing.streetNumber), | |
streetName = diff.streetName.getOrElse(existing.streetName), | |
city = diff.city.getOrElse(existing.city), | |
country = diff.country.getOrElse(existing.country) | |
) | |
) | |
case (Some(_), None) => None | |
case ( | |
None, | |
Some( | |
AddressDiff(Diff.Change(streetNum), Diff.Change(streetName), Diff.Change(city), Diff.Change(country)) | |
) | |
) => | |
Some(Address(streetNum, streetName, city, country)) | |
case (None, _) => None | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import shapeless._ | |
import java.time._ | |
sealed trait Diff[+A] { self => | |
def getOrElse[A0 >: A](default: A0): A0 = self match { | |
case Diff.Identical => default | |
case Diff.Change(value) => value | |
} | |
} | |
object Diff { | |
case object Identical extends Diff[Nothing] | |
final case class Change[A](value: A) extends Diff[A] | |
def change[A](value: A): Diff[A] = Change(value) | |
} | |
trait Delta[In] { | |
type Out | |
def apply(existing: In, incoming: In): Out | |
} | |
object Delta extends DeltaLowPriority { | |
type Aux[I, O] = Delta[I] { | |
type Out = O | |
} | |
def apply[A](implicit proof: Lazy[Delta[A]]): Delta.Aux[A, proof.value.Out] = proof.value | |
private def instance[A](same: (A, A) => Boolean): Delta.Aux[A, Diff[A]] = new Delta[A] { | |
type Out = Diff[A] | |
override def apply(existing: A, incoming: A): Diff[A] = | |
if (same(existing, incoming)) Diff.Identical | |
else Diff.Change(incoming) | |
} | |
implicit val booleanDelta: Aux[Boolean, Diff[Boolean]] = instance[Boolean](_ == _) | |
implicit val stringDelta: Aux[String, Diff[String]] = instance[String](_ == _) | |
implicit val intDelta: Aux[Int, Diff[Int]] = instance[Int](_ == _) | |
implicit val localDateDelta: Aux[LocalDate, Diff[LocalDate]] = instance[LocalDate](_ isEqual _) | |
implicit val localDateTimeDelta: Aux[LocalDateTime, Diff[LocalDateTime]] = instance[LocalDateTime](_ isEqual _) | |
implicit def listDelta[A](implicit | |
diffProof: Aux[A, Diff[A]], | |
orderProof: Ordering[A] | |
): Aux[List[A], Diff[List[A]]] = | |
instance[List[A]] { (incoming, existing) => | |
val iS = incoming.sorted | |
val eS = existing.sorted | |
iS == eS | |
} | |
} | |
// Implicit prioritization tricks are used to allow the user to override datatypes | |
trait DeltaLowPriority { | |
// We keep these derivations in low priority so if you provide more specific proof, it will prefer that over these | |
implicit val hnilDelta: Delta.Aux[HNil, HNil] = new Delta[HNil] { | |
// This must be HNil or the HList will not terminate and the code will not compile | |
type Out = HNil | |
override def apply(existing: HNil, incoming: HNil): HNil = existing | |
} | |
implicit def optionDeltaHList[A <: HList](implicit | |
deltaA: Lazy[Delta[A]] | |
): Aux[Option[A], Option[deltaA.value.Out]] = | |
new Delta[Option[A]] { | |
override type Out = Option[deltaA.value.Out] | |
override def apply(existing: Option[A], incoming: Option[A]): Out = (existing, incoming) match { | |
case (Some(e), Some(i)) => Option(deltaA.value.apply(e, i)) | |
case (_, _) => None | |
} | |
} | |
implicit def optionDelta[A](implicit proofNotHList: A =:!= HList): Aux[Option[A], Diff[Option[A]]] = | |
new Delta[Option[A]] { | |
type Out = Diff[Option[A]] | |
override def apply(existing: Option[A], incoming: Option[A]): Diff[Option[A]] = | |
if (existing != incoming) Diff.Change(incoming) | |
else Diff.Identical | |
} | |
implicit def hconsDelta[H, T <: HList](implicit | |
proofHead: Lazy[Delta[H]], | |
proofTail: Lazy[Delta[T] { type Out <: HList }] | |
): Delta.Aux[H :: T, proofHead.value.Out :: proofTail.value.Out] = new Delta[H :: T] { | |
override type Out = proofHead.value.Out :: proofTail.value.Out | |
override def apply(existing: H :: T, incoming: H :: T): proofHead.value.Out :: proofTail.value.Out = { | |
val diffHead = proofHead.value.apply(existing.head, incoming.head) | |
val diffTail = proofTail.value.apply(existing.tail, incoming.tail) | |
diffHead :: diffTail | |
} | |
} | |
implicit def genericDelta[A, Repr, Out](implicit | |
gen: Generic.Aux[A, Repr], | |
delta: Lazy[Delta.Aux[Repr, Out]] | |
): Delta.Aux[A, Out] = | |
new Delta[A] { | |
override type Out = delta.value.Out | |
override def apply(existing: A, incoming: A): Out = | |
delta.value.apply(gen.to(existing), gen.to(incoming)) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
object Example { | |
final case class Person( | |
firstName: String, | |
lastName: String, | |
age: Int, | |
address: Option[Address], | |
optionalInfo: Option[Int], | |
address2: Address | |
) | |
val deltaPerson = Delta[Person] | |
val mergePerson = Merge[Person] | |
val p1 = Person( | |
"Calvin", | |
"Fernandes", | |
29, | |
Option(Address(73, "Vessel Crescent", "Scarborough", "Canada")), | |
None, | |
Address(73, "Vessel Crescent", "Scarborough", "Canada") | |
) | |
val p1_1 = p1.copy(optionalInfo = Some(10)) | |
val p2 = Person( | |
"Calvin", | |
"Fernandes", | |
30, | |
Option(Address(75, "Vessel Crescent", "Scarborough", "Canada")), | |
None, | |
Address(75, "Vessel Crescent", "Toronto", "Canada") | |
) | |
val personDeltaInstanceP1ToP11 = deltaPerson(p1, p1_1) | |
val personDeltaInstanceP1toP2 = deltaPerson(p1, p2) | |
val step1 = mergePerson(p1, personDeltaInstanceP1ToP11) | |
val step2 = mergePerson(step1, personDeltaInstanceP1toP2) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import shapeless._ | |
import java.time._ | |
trait Merge[A] { self => | |
type D | |
def apply(base: A, diff: D): A | |
} | |
object Merge extends MergeLowPriorityInstance { | |
type Aux[A, Diff] = Merge[A] { | |
type D = Diff | |
} | |
def apply[A](implicit proof: Lazy[Merge[A]]): Merge.Aux[A, proof.value.D] = proof.value | |
private def instance[A]: Merge.Aux[A, Diff[A]] = new Merge[A] { | |
type D = Diff[A] | |
override def apply(base: A, diff: D): A = diff match { | |
case Diff.Identical => base | |
case Diff.Change(value) => value | |
} | |
} | |
implicit val booleanMerge: Merge.Aux[Boolean, Diff[Boolean]] = instance[Boolean] | |
implicit val stringMerge: Merge.Aux[String, Diff[String]] = instance[String] | |
implicit val intMerge: Merge.Aux[Int, Diff[Int]] = instance[Int] | |
implicit val localDateMerge: Merge.Aux[LocalDate, Diff[LocalDate]] = instance[LocalDate] | |
implicit val localDateTimeMerge: Merge.Aux[LocalDateTime, Diff[LocalDateTime]] = instance[LocalDateTime] | |
} | |
// Implicit prioritization tricks are used to allow the user to override datatypes | |
trait MergeLowPriorityInstance { | |
implicit def optionMerge[A](implicit proofNotHList: A =:!= HList): Merge.Aux[Option[A], Diff[Option[A]]] = | |
new Merge[Option[A]] { | |
type D = Diff[Option[A]] | |
override def apply(base: Option[A], diff: Diff[Option[A]]): Option[A] = diff match { | |
case Diff.Identical => base | |
case Diff.Change(value) => value | |
} | |
} | |
implicit def optionMergeHList[A <: HList](implicit | |
mergeA: Lazy[Merge[A]] | |
): Merge.Aux[Option[A], Option[mergeA.value.D]] = | |
new Merge[Option[A]] { | |
override type D = Option[mergeA.value.D] | |
override def apply(base: Option[A], diff: D): Option[A] = | |
(base, diff) match { | |
case (Some(b), Some(d)) => Some(mergeA.value.apply(b, d)) | |
case (Some(b), None) => Some(b) | |
case (None, None) => None | |
} | |
} | |
implicit val hnilMerge: Merge.Aux[HNil, HNil] = new Merge[HNil] { | |
type D = HNil | |
override def apply(base: HNil, diff: D): HNil = HNil | |
} | |
implicit def hconsMerge[H, T <: HList](implicit | |
proofHead: Lazy[Merge[H]], | |
proofTail: Lazy[Merge[T] { type D <: HList }] | |
): Merge.Aux[H :: T, proofHead.value.D :: proofTail.value.D] = new Merge[H :: T] { | |
type D = proofHead.value.D :: proofTail.value.D | |
override def apply(base: H :: T, diff: D): H :: T = { | |
val resHead: H = proofHead.value.apply(base.head, diff.head) | |
val resTail: T = proofTail.value.apply(base.tail, diff.tail) | |
resHead :: resTail | |
} | |
} | |
implicit def genericMerge[A, Repr](implicit | |
gen: Generic.Aux[A, Repr], | |
merge: Lazy[Merge[Repr]] | |
): Merge.Aux[A, merge.value.D] = | |
new Merge[A] { | |
override type D = merge.value.D | |
override def apply(base: A, diff: D): A = { | |
val repr: Repr = merge.value.apply(gen.to(base), diff) | |
gen.from(repr) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment