Last active
          August 15, 2018 00:01 
        
      - 
      
- 
        Save afsalthaj/3b2ec490cec1b4a74d2a706c290af203 to your computer and use it in GitHub Desktop. 
  
    
      This file contains hidden or 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
    
  
  
    
  | 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")) | |
| } | |
| ////// | 
  
    Sign up for free
    to join this conversation on GitHub.
    Already have an account?
    Sign in to comment
  
            
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.