Skip to content

Instantly share code, notes, and snippets.

@pheymann
Last active June 12, 2020 22:21
Show Gist options
  • Save pheymann/c3eb42c64e9d2e8bf31dbbe8150580c2 to your computer and use it in GitHub Desktop.
Save pheymann/c3eb42c64e9d2e8bf31dbbe8150580c2 to your computer and use it in GitHub Desktop.
Generic `case class` instance diff's (aka. comparing instance fields and output differences)
import shapeless._
import shapeless.labelled.FieldType
import shapeless.record._
trait GenericDiff[H <: HList] {
// syntactic sugar
type HI = H
// compares field values and returns the field name with values if they differ
def apply(l: HI, r: HI): Map[String, Any]
}
trait LowPriorityGenericDiff {
implicit val hnilGenDiff: GenericDiff[HNil] = new GenericDiff[HNil] {
def apply(l: HNil, r: HNil): Map[String, Any] =
Map.empty
}
implicit def basicGenDiff[K <: Symbol, V, T <: HList](implicit wit: Witness.Aux[K], genDiff: Lazy[GenericDiff[T]])
: GenericDiff[FieldType[K, V] :: T] =
new GenericDiff[FieldType[K, V] :: T] {
def apply(l: HI, r: HI): Map[String, Any] = {
// only report fields which differ
// we just use method `equal` here for comparison, we could also provide a typeclass
if (l.head != r.head) genDiff.value(l.tail, r.tail) + (wit.value.name -> (l.head, r.head))
else genDiff.value(l.tail, r.tail)
}
}
}
trait MediumPriorityGenericDiff extends LowPriorityGenericDiff {
implicit def nestedGenDiff[K <: Symbol, V, R <: HList, T <: HList](implicit wit: Witness.Aux[K],
gen: LabelledGeneric.Aux[V, R],
genDiffH: Lazy[GenericDiff[R]],
genDiffT: Lazy[GenericDiff[T]])
: GenericDiff[FieldType[K, V] :: T] = new GenericDiff[FieldType[K, V] :: T] {
def apply(l: HI, r: HI): Map[String, Any] = {
val diffs = genDiffH.value(gen.to(l.head), gen.to(r.head))
// only report fields which differ
if (diffs.isEmpty) genDiffT.value(l.tail, r.tail)
else genDiffT.value(l.tail, r.tail) + (wit.value.name -> diffs)
}
}
}
trait HighPriorityGenericDiff extends MediumPriorityGenericDiff {
implicit def seqGenDiff[K <: Symbol, V, R <: HList, T <: HList](implicit wit: Witness.Aux[K],
gen: LabelledGeneric.Aux[V, R],
genDiffH: Lazy[GenericDiff[R]],
genDiffT: Lazy[GenericDiff[T]])
: GenericDiff[FieldType[K, Seq[V]] :: T] = new GenericDiff[FieldType[K, Seq[V]] :: T] {
def apply(l: HI, r: HI): Map[String, Any] = {
// we expect elements of `l` and `r` to be in the same order
val diffs = l.head.zip(r.head)
.map { case (valueL, valueR) => genDiffH.value(gen.to(valueL), gen.to(valueR)) }
.filter(_.nonEmpty)
// only report fields which differ
if (diffs.isEmpty) genDiffT.value(l.tail, r.tail)
else genDiffT.value(l.tail, r.tail) + (wit.value.name -> diffs)
}
}
/* add instances for other data structures here, eg. Tree, Map, Vector, ... */
}
object Test extends HighPriorityGenericDiff {
final case class User(name: String, age: Int)
final case class Group(id: Long, users: Seq[User])
// helper
def diff[A, H <: HList](l: A, r: A)(implicit gen: LabelledGeneric.Aux[A, H],
genDiff: Lazy[GenericDiff[H]]): Map[String, Any] =
genDiff.value(gen.to(l), gen.to(r))
def main(args: Array[String]): Unit = {
val usr0 = User("foo", 1)
val usr1 = User("bar", 2)
val usr2 = User("bar", 3)
println("equal users ", diff(usr0, usr0)) // (equal users: ,Map())
println("unequal users ", diff(usr0, usr1)) // (unequal users: ,Map(age -> (1,2), name -> (foo,bar)))
println("unequal group ", diff(Group(0l, Seq(usr0, usr1)), Group(0l, Seq(usr0, usr2)))) // (unequal group: ,Map(users -> List(Map(age -> (2,3)))))
}
}
@sdas987
Copy link

sdas987 commented Jun 12, 2020

Hi, trying to use this example, am getting an error like
could not find Lazy implicit value of type GenericDiff[H]

Do you know what might be going wrong. Thanks

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