Skip to content

Instantly share code, notes, and snippets.

@loicknuchel
Created November 8, 2018 12:43
Show Gist options
  • Save loicknuchel/d5a32a4778ea3f610fbed97b8095c4e2 to your computer and use it in GitHub Desktop.
Save loicknuchel/d5a32a4778ea3f610fbed97b8095c4e2 to your computer and use it in GitHub Desktop.
Case class migration
trait Migrator[A, B] {
def migrate(a: A): B
}
object Migrator {
def apply[A, B](implicit m: Migrator[A, B]): Migrator[A, B] = m
implicit def product[A, B, AR <: HList, BR <: HList, Common <: HList, Added <: HList, Unaligned <: HList]
(implicit
aGen: LabelledGeneric.Aux[A, AR], // generic representation of A (AR is the Repr of A)
bGen: LabelledGeneric.Aux[B, BR], // generic representation of B (BR is the Repr of B)
inter: ops.hlist.Intersection.Aux[AR, BR, Common], // build Common HList with elements both in AR and BR HLists
diff: ops.hlist.Diff.Aux[BR, Common, Added], // build Added HList with elements of BR not in AR
monoid: Monoid[Added], // find a Monoid of Added HList (used for zero instance)
prepend: ops.hlist.Prepend.Aux[Added, Common, Unaligned], // merge Added and Common HLists into Unaligned
align: ops.hlist.Align[Unaligned, BR] // reorder Unaligned elements to match BR HList
): Migrator[A, B] = (a: A) => bGen.from(align.apply(prepend.apply(monoid.empty, inter.apply(aGen.to(a)))))
def createMonoid[A](zero: A)(add: (A, A) => A): Monoid[A] =
new Monoid[A] {
def empty: A = zero
def combine(x: A, y: A): A = add(x, y)
}
implicit val hnilMonoid: Monoid[HNil] =
createMonoid[HNil](HNil)((_, _) => HNil)
implicit def hlistMonoid[K <: Symbol, H, T <: HList](implicit
hMonoid: Lazy[Monoid[H]],
tMonoid: Monoid[T]): Monoid[FieldType[K, H] :: T] =
createMonoid(field[K](hMonoid.value.empty) :: tMonoid.empty) {
(x, y) => field[K](hMonoid.value.combine(x.head, y.head)) :: tMonoid.combine(x.tail, y.tail)
}
implicit class MigratorOps[A](a: A) {
def migrateTo[B](implicit m: Migrator[A, B]): B = m.migrate(a)
object withFields extends RecordArgs {
def applyRecord[AR <: HList, R <: HList, Res <: HList](r: R)
(implicit
aGen: LabelledGeneric.Aux[A, AR],
prepend: ops.hlist.Prepend.Aux[AR, R, Res]): Res =
prepend.apply(aGen.to(a), r)
}
}
}
import java.sql.Timestamp
import java.time.Instant
import org.scalatest.{FunSpec, Matchers}
class MigratorSpec extends FunSpec with Matchers {
import Migrator._
case class Src(a: String, b: Int)
val src = Src("abc", 2)
describe("Migrator") {
it("should convert identical case classes") {
case class Dest(a: String, b: Int)
val dest = Dest(src.a, src.b)
src.migrateTo[Dest] shouldBe dest
}
it("should convert classes with different parameter order") {
case class Dest(b: Int, a: String)
val dest = Dest(src.b, src.a)
src.migrateTo[Dest] shouldBe dest
}
it("should convert to classes with less arguments") {
case class Dest(a: String)
val dest = Dest(src.a)
src.migrateTo[Dest] shouldBe dest
}
it("should convert to classes with more arguments giving missing ones") {
case class Dest(a: String, b: Int, c: Boolean)
val dest = Dest(src.a, src.b, c = true)
//FIXME src.migrateTo[Dest](c = true) shouldBe dest
}
ignore("should perform basic type transformations") {
case class Dest(a: String, b: String)
val dest = Dest(src.a, src.b.toString)
//FIXME src.migrateTo[Dest] shouldBe dest
}
ignore("should perform type transformation with Option") {
case class Dest(a: String, b: Option[Int])
val dest = Dest(src.a, Some(src.b))
//FIXME src.migrateTo[Dest] shouldBe dest
}
ignore("should perform additional type transformations") {
//implicit val stringToInt: Migrator[String, Int] = Migrator.create(_.length)
case class Dest(a: Int, b: Int)
val dest = Dest(src.a.length, src.b)
//FIXME src.migrateTo[Dest] shouldBe dest
}
it("should add missing optional attributes") {
import cats.instances.all._
case class Dest(a: String, b: Int, c: Option[Int])
val dest = Dest(src.a, src.b, c = None)
src.migrateTo[Dest] shouldBe dest
}
ignore("should convert to nested classes") {
case class Nest(b: Int)
case class Dest(a: String, nest: Nest)
val dest = Dest(src.a, Nest(src.b))
//FIXME src.migrateTo[Dest] shouldBe dest
}
ignore("should convert from nested classes") {
case class Nest(b: Int)
case class Dest(a: String, nest: Nest)
val dest = Dest(src.a, Nest(src.b))
//FIXME dest.migrateTo[Src] shouldBe src
}
ignore("should convert complex classes") {
case class Id(value: String)
case class Wrapped(value: Int)
case class Source(a: Id, w: Wrapped, m: String, n: String, i: Instant, j: Instant)
case class Nested(i: Timestamp, j: Option[Timestamp], k: Option[Timestamp])
case class Destination(a: String, w: Int, m: String, n: Option[String], nest: Nested)
val src = Source(Id("id"), Wrapped(2), "m", "n", Instant.now(), Instant.now().plusSeconds(60))
val dest = Destination(src.a.value, src.w.value, src.m, Some(src.n), Nested(Timestamp.from(src.i), Some(Timestamp.from(src.j)), None))
//FIXME src.migrateTo[Destination] shouldBe dest
//FIXME dest.migrateTo[Source] shouldBe src
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment