Created
November 8, 2018 12:43
-
-
Save loicknuchel/d5a32a4778ea3f610fbed97b8095c4e2 to your computer and use it in GitHub Desktop.
Case class migration
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
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) | |
} | |
} | |
} |
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
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