Skip to content

Instantly share code, notes, and snippets.

@afsalthaj
Last active August 15, 2018 00:01
Show Gist options
  • Save afsalthaj/3b2ec490cec1b4a74d2a706c290af203 to your computer and use it in GitHub Desktop.
Save afsalthaj/3b2ec490cec1b4a74d2a706c290af203 to your computer and use it in GitHub Desktop.
package delta
// Use shows instance instead of toString on even primitives.
import shapeless.{::, HList, HNil, LabelledGeneric, Lazy, Witness}
import shapeless.labelled.FieldType
import cats.evidence.{As, Is}
import Delta.Meta
import cats.Eq
import cats.data.Ior
import cats.implicits._
/**
* Recursively looks through data structures of any complexity
* and tracks down the changes as a Meta.
*
* The comparison are restricted using either
* HasName or cats.Eq with in the shapes.
*/
trait FindDeltaMeta[A] {
def apply(a: A, b: A): Delta.Meta
}
object FindDeltaMeta extends LowPriorityInstances0 {
def apply[A](implicit ev: FindDeltaMeta[A]): FindDeltaMeta[A] = ev
}
trait LowPriorityInstances0 extends LowPriorityInstances1 {
implicit def hNilFindDeltaMeta: FindDeltaMeta[HNil] =
(_, _) => Delta.Meta.empty
implicit def findDiffA[A, R <: HList](
implicit E: LabelledGeneric.Aux[A, R],
D: FindDeltaMeta[R]
): FindDeltaMeta[A] = {
(a, b) => D.apply(E.to(a), E.to(b))
}
}
trait LowPriorityInstances1 extends LowPriorityInstances2 {
implicit def hListWithSimpleAnyVal[A: HasName, K <: Symbol, T <: HList](
implicit
witness: Witness.Aux[K],
IsAnyVal: A As AnyVal,
D: Lazy[FindDeltaMeta[T]],
): FindDeltaMeta[FieldType[K, A] :: T] =
(a, b) =>
(if (HasName[A].name(a.head: A) === HasName[A].name(b.head: A)) {
Meta.empty
} else {
Meta(
witness.value.name, Ior.Both(
HasName[A].name(a.head: A),
HasName[A].name(b.head: A)
)
)
}
) ++ D.value.apply(a.tail, b.tail)
}
trait LowPriorityInstances2 extends LowPriorityInstances3 {
implicit def hLIstWithFofHListInsideItOption[A, K <: Symbol, H, InnerT <: HList, T <: HList](
implicit
witness: Witness.Aux[K],
IsList: H As Option[A],
eachH: LabelledGeneric.Aux[A, InnerT],
HN: HasName[A],
D: Lazy[FindDeltaMeta[T]],
E: Lazy[FindDeltaMeta[InnerT]]
): FindDeltaMeta[FieldType[K, H] :: T] =
(a, b) => {
val leftOption = a.head.asInstanceOf[Option[A]]
val rightOption = b.head.asInstanceOf[Option[A]]
val r =
(leftOption, rightOption) match {
case (Some(la), Some(ra)) => E.value.apply(eachH.to(la), eachH.to(ra)).prependToKey(HasName[A].name(la))
case (Some(la), None) => Meta(HasName[A].name(la), Ior.Left(la.toString))
case (None, Some(ra)) => Meta(HasName[A].name(ra), Ior.Right(ra.toString))
case _ => Meta.empty
}
r.prependToKey(witness.value.name) ++ D.value.apply(a.tail, b.tail)
}
}
trait LowPriorityInstances3 extends LowPriorityInstances4 {
implicit def hLIstWithFofHListInsideIt[A, K <: Symbol, H, InnerT <: HList, T <: HList](
implicit
witness: Witness.Aux[K],
IsList: H As List[A],
eachH: LabelledGeneric.Aux[A, InnerT],
HN: HasName[A],
D: Lazy[FindDeltaMeta[T]],
E: Lazy[FindDeltaMeta[InnerT]]
): FindDeltaMeta[FieldType[K, H] :: T] =
(a, b) => {
val leftList = a.head.asInstanceOf[List[A]]
val rightList = b.head.asInstanceOf[List[A]]
val namesOnLeft = leftList.map(t => HasName[A].name(t))
val toBeComparedOnRight =
rightList.filter {
bb => namesOnLeft.containsSlice(List(HasName[A].name(bb)))
}
// convert toString to shows and print it nicely
val newData =
rightList.filterNot(
t => toBeComparedOnRight.map(t => HasName[A].name(t)).containsSlice(List(HasName[A].name(t)))
).map(t => Meta(HasName[A].name(t), Ior.Right(t.toString)).prependToKey(witness.value.name))
// convert toString to shows and print it nicely
val deletedData =
leftList.filterNot(t => rightList.map(t => HasName[A].name(t)).containsSlice(List(HasName[A].name(t))))
.map(t => Meta(HasName[A].name(t), Ior.Left(t.toString)).prependToKey(witness.value.name))
val compareResourcesWithSameName =
leftList.zip(toBeComparedOnRight).map {
case (aa, bb) =>
E.value.apply(eachH.to(aa), eachH.to(bb)).prependToKey(HasName[A].name(aa))
}
deletedData.fold(Nil)(_ ++ _) ++
newData.fold(Nil)(_ ++ _) ++
compareResourcesWithSameName.fold(Nil)(_ ++ _).prependToKey(witness.value.name) ++
D.value.apply(a.tail, b.tail)
}
}
trait LowPriorityInstances4 extends LowPriorityInstances5 {
implicit def hListNamerWithHListInsideOfInsideOf[K <: Symbol, H, InnerT <: HList, T <: HList](
implicit
witness: Witness.Aux[K],
eachH: LabelledGeneric.Aux[H, InnerT],
H: HasName[H],
D: Lazy[FindDeltaMeta[T]],
E: Lazy[FindDeltaMeta[InnerT]]
): FindDeltaMeta[FieldType[K, H] :: T] =
(a, b) => {
val diff =
if (H.name(a.head) === H.name(b.head)) {
E.value.apply(eachH.to(a.head.asInstanceOf[H]), eachH.to(b.head.asInstanceOf[H]))
.prependToKey(HasName[H].name(b.head)).prependToKey(witness.value.name)
} else {
// convert toString to shows and print it nicely
Meta(HasName[H].name(b.head), Ior.Right(b.head.toString)).prependToKey(witness.value.name) ++
Meta(HasName[H].name(a.head), Ior.Left(a.head.toString)).prependToKey(witness.value.name)
}
diff ++ D.value.apply(a.tail, b.tail)
}
}
trait LowPriorityInstances5 {
implicit def simpleHList[A, K <: Symbol, H: Eq, R <: HList, T <: HList](
implicit
witness: Witness.Aux[K],
D: Lazy[FindDeltaMeta[T]]
): FindDeltaMeta[FieldType[K, H] :: T] =
(a, b) => {
(if ((a.head: H) === (b.head: H)) {
Meta.empty
} else {
Meta(
witness.value.name,
Ior.Both((a.head: H).toString, (b.head: H).toString)
)
}
) ++ D.value.apply(a.tail, b.tail)
}
}
////////
import cats.evidence.As
import shapeless.ops.hlist.IsHCons
import shapeless.{HList, LabelledGeneric}
trait HasName[A] { self =>
def name(a: A): String
}
object HasName {
def apply[A](implicit ev: HasName[A]): HasName[A] = ev
implicit val intName: HasName[Int] = _.toString
implicit val stringName: HasName[String] = _.toString
implicit val doubleName: HasName[Double] = _.toString
implicit val longName: HasName[Long] = _.toString
implicit def hListWithSimpleAnyVal[A, K <: Symbol, T <: HList](
implicit
IsAnyVal: A As AnyVal,
L: LabelledGeneric.Aux[A, T],
E: IsHCons[T]
): HasName[A] = a => L.to(a).head.toString
}
////////
///////
import cats.Show
import cats.data.Ior
final case class UpdateInfo private (key: String, action: String, previous: String, newValue: String)
object UpdateInfo {
/// To make terraformies happy :) Consider this to be the end of the world ugliness
def mk(key: String, previousNewValue: String Ior String): UpdateInfo =
previousNewValue match {
case Ior.Both(a, b) => UpdateInfo(key, "update", a, b)
case Ior.Left(a) => UpdateInfo(key, "delete", a, "")
case Ior.Right(b) => UpdateInfo(key, "created", "", b)
}
implicit val updateInfo: Show[UpdateInfo] =
Show.show[UpdateInfo](a =>
s"${a.key} : ${a.previous} ---> ${a.newValue}"
)
}
//////
//////
type Meta = List[UpdateInfo]
object Meta {
/**
* To have the niceness of:
* Meta("key", "old", "new") ++ Meta("anotherkey", "old", "new")
*/
def apply(key: String, previousAndNewValue: String Ior String): Meta =
List(UpdateInfo.mk(key, previousAndNewValue))
def prependToKey(s: String, ma: Meta): Meta =
ma.map { u => u.copy(key = s + "." + u.key) }
def appendToKey(s: String, ma: Meta): Meta =
ma.map { u => u.copy(key = u.key + "." + s) }
def empty: Meta = Nil
implicit val metaShow: Show[Meta] =
Show.show(_.map(_.show).mkString("\n"))
}
//////
@afsalthaj
Copy link
Author

afsalthaj commented Aug 14, 2018

This recursively diff two data structures of any complexity, such that if there are inner nested case class, you need to provide HasName instance in order for the diff to be calculated only if the names are same (id, indeed). Otherwise tracks them as new or deleted. It works for inner to inner (nested) case classes of any complexity, lists (with nested case classes) and option. It also handles AnyVal nicely such that it doesn't consider them to be products and gives you comparison at primitive level.

case class Inner(x1: String, name: String)
case class Afsal(x1: String, y: Inner)

implicit val hasNameInner: HasName[Inner] = _.name

val x = Afsal(....)
val y = Afsal(....)

`FindDeltaMeta[Afsal].apply(x, y)`

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment