Skip to content

Instantly share code, notes, and snippets.

@davydkov
Created November 25, 2016 12:06
Show Gist options
  • Save davydkov/1f8d1fdc24adc25d15a29655a89a6879 to your computer and use it in GitHub Desktop.
Save davydkov/1f8d1fdc24adc25d15a29655a89a6879 to your computer and use it in GitHub Desktop.
Scala morphism with shapeless
import shapeless._
import shapeless.labelled._
import shapeless.ops.record.Merger
import shapeless.syntax.SingletonOps
import scala.annotation.implicitNotFound
import scala.collection.generic.CanBuildFrom
import scala.language.experimental.macros
import scala.language.higherKinds
package object morph {
@implicitNotFound(msg = "No morphism found for type ${A} to ${B}")
trait Morphism[A, B] {
def morph(a: A): B
}
object Morphism extends LowPriorityMorphism {
/**
* Build Morphism from function
*/
def apply[A, B](fn: A => B): Morphism[A, B] = new Morphism[A, B] {
override def morph(a: A): B = fn(a)
}
/**
* For values of the same types
*/
implicit def equalTypeMorph[A]: Morphism[A, A] = Morphism(a => a)
}
trait LowPriorityMorphism extends LowestPriorityMorphism {
/**
* When we are at the end of target B
*/
implicit def hnilMorph[A <: HList]: Morphism[A, HNil] = Morphism(_ => HNil)
/**
* Derive morphism for options
* Option[A] => Option[B]
*/
implicit def morphOptionals[
A,
B
](implicit
morphism: Lazy[Morphism[A, B]]
): Morphism[Option[A], Option[B]] = {
Morphism(a => a.map(morphism.value.morph))
}
/**
* Derive morphism for iterables
* Iterable[A] => Iterable[B]
*/
implicit def morphIterables[
A,
B,
C[A] <: Iterable[A]
](implicit
morphism: Lazy[Morphism[A, B]],
cbf: CanBuildFrom[C[B], B, C[B]]
): Morphism[C[A], C[B]] =
Morphism({ in: C[A] =>
def builder = {
// extracted to keep method size under 35 bytes, so that it can be JIT-inlined
val b = cbf()
b.sizeHint(in)
b
}
val morpher = morphism.value
val b = builder
for (x <- in) b += morpher.morph(x)
b.result()
})
/**
* Recursively walks though the fields on target HList and
* finds it in the source. Requires morphism for head and tail.
*/
implicit def recursiveMorphFields[
Id,
BV, // Value type of field in B
AV, // Value Type of field in A
ARepr <: HList, // HList representation of A
BT <: HList // HList representation of tail of B
](implicit
select: ops.record.Selector.Aux[ARepr, Id, AV],
fieldMorphism: Lazy[Morphism[AV, BV]],
tailMorphism: Lazy[Morphism[ARepr, BT]]
): Morphism[ARepr, FieldType[Id, BV] :: BT] =
Morphism({ a: ARepr =>
field[Id](fieldMorphism.value.morph(select(a))) :: tailMorphism.value.morph(a)
})
}
trait LowestPriorityMorphism {
implicit def deriveMorphism[
A, // Source type
B, // Target type
AR <: HList, // HList representation of A
BR <: HList // HList representation of B
](implicit
genA: LabelledGeneric.Aux[A, AR],
genB: LabelledGeneric.Aux[B, BR],
morphism: Lazy[Morphism[AR, BR]]
): Morphism[A, B] = Morphism(a => {
genB from morphism.value.morph(genA.to(a))
})
}
class MorphAndMerge[A, B](val source: A) {
def apply[
K,
V,
AR <: HList, // HList representation of A
BR <: HList, // HList representation of B
Fields <: HList, // Extra fields to merge
Merged <: HList // A merged with Fields
](fields: Fields)
(implicit
genA: LabelledGeneric.Aux[A, AR],
genB: LabelledGeneric.Aux[B, BR],
merger: Merger.Aux[AR, Fields, Merged],
morphism: Lazy[Morphism[Merged, BR]]
): B = {
genB from morphism.value.morph(merger(genA.to(source), fields))
}
}
implicit class MorphismOps[A](val a: A) extends AnyVal {
/**
* Please refer [[MorphismTest]] for examples
*/
def morphTo[B](implicit morphism: Morphism[A, B]): B = {
morphism.morph(a)
}
/**
* If you need to add overrides to fields from A, than
*
* source.morphMerge[B](
* 'field1 ->> "Value" ::
* 'field2 ->> OtherValue ::
* HNil
* )
*/
def morphMerge[B]: MorphAndMerge[A, B] = new MorphAndMerge[A, B](a)
}
}
class MorphismTest extends FlatSpec with Matchers {
case class One(
value1: String,
value2: String,
value3: Int
)
case class Two(
value1: String,
value2: String,
value3: Int
)
// Change field order
case class Three(
value2: String,
value3: Int,
value1: String
)
// Add extra field
case class Four(
value1: String,
value2: String,
value3: Int,
extra: Double
)
/* ==========================================================================
========================================================================== */
behavior of "Shapeless morph (Flat)"
it should "... copy all properties from A to B (same order of fields)" in {
val one = One("v1", "v2", 3)
val two = one.morphTo[Two]
(one.value1, one.value2, one.value3) shouldEqual ((two.value1, two.value2, two.value3))
}
it should "... copy all properties from A to B (different order of fields)" in {
val one = One("v1", "v2", 3)
val three = one.morphTo[Three]
(one.value1, one.value2, one.value3) shouldEqual(three.value1, three.value2, three.value3)
}
it should "... copy all properties from A to B (omit extra fields)" in {
val four = Four("v1", "v2", 3, 2)
val one = four.morphTo[One]
(one.value1, one.value2, one.value3) shouldEqual(four.value1, four.value2, four.value3)
}
/* ==========================================================================
========================================================================== */
behavior of "Shapeless morph (with Seq)"
case class OneSeq(value1: String,
value2: Seq[String])
case class TwoSeq(value2: Seq[String],
value1: String)
it should "... copy all properties from A to B (with sequence fields)" in {
val one = OneSeq("v1", Seq("1", "2"))
val two = one.morphTo[TwoSeq]
(one.value1, one.value2) shouldEqual(two.value1, two.value2)
}
/* ==========================================================================
========================================================================== */
behavior of "Shapeless morph (with Option)"
case class OneOpt(value1: String,
value2: Option[String])
case class TwoOpt(value2: Option[String],
value1: String)
it should "... copy all properties from A to B (with option fields)" in {
val one = OneSeq("v1", Seq("1", "2"))
val two = one.morphTo[TwoSeq]
(one.value1, one.value2) shouldEqual(two.value1, two.value2)
}
/* ==========================================================================
========================================================================== */
behavior of "Shapeless morph (Deep case-classes)"
case class Leaf1(value1: String, value2: Int)
case class Root1(value1: String, leaf: Leaf1)
case class Leaf2(value1: String, value2: Int)
case class Root2(leaf: Leaf2, value1: String)
it should "... copy all properties from A to B" in {
val root1 = Root1("root1", Leaf1("v1", 2))
val root2 = root1.morphTo[Root2]
root1.value1 shouldEqual root2.value1
(root1.leaf.value1, root1.leaf.value2) shouldEqual(root2.leaf.value1, root2.leaf.value2)
}
/* ==========================================================================
========================================================================== */
behavior of "Shapeless morph (Deep case-classes with Options)"
case class RootOpt1(value1: String, opt: Option[Root1])
case class RootOpt2(opt: Option[Root2], value1: String)
it should "... copy all properties from A to B" in {
val root1 = RootOpt1("top", Some(Root1("root1", Leaf1("v1", 2))))
val root2 = root1.morphTo[RootOpt2]
root2.value1 shouldEqual root2.value1
val el1 = root1.opt.get
val el2 = root2.opt.get
el1.value1 shouldEqual el2.value1
(el1.leaf.value1, el1.leaf.value2) shouldEqual(el2.leaf.value1, el2.leaf.value2)
}
/* ==========================================================================
========================================================================== */
behavior of "Shapeless morph (Deep case-classes with Sequences)"
case class RootSeq1(value1: String, roots: Seq[Root1])
case class RootSeq2(roots: Seq[Root2], value1: String)
it should "... copy all properties from A to B" in {
val root1 = RootSeq1("top", Seq(Root1("root1", Leaf1("v1", 2))))
val root2 = root1.morphTo[RootSeq2]
root2.value1 shouldEqual root2.value1
val el1 = root1.roots.head
val el2 = root2.roots.head
el1.value1 shouldEqual el2.value1
(el1.leaf.value1, el1.leaf.value2) shouldEqual(el2.leaf.value1, el2.leaf.value2)
}
/* ==========================================================================
========================================================================== */
behavior of "Shapeless morph (Deep case-classes with Set)"
case class RootSet1(value1: String, roots: Set[Root1])
case class RootSet2(roots: Set[Root2], value1: String)
it should "... copy all properties from A to B" in {
val root1 = RootSet1("top", Set(Root1("root1", Leaf1("v1", 2))))
val root2 = root1.morphTo[RootSet2]
root2.value1 shouldEqual root2.value1
val el1 = root1.roots.head
val el2 = root2.roots.head
el1.value1 shouldEqual el2.value1
(el1.leaf.value1, el1.leaf.value2) shouldEqual(el2.leaf.value1, el2.leaf.value2)
}
/* ==========================================================================
========================================================================== */
behavior of "Shapeless morph (Deep case-classes with transformation of nested types)"
it should "... copy all properties from A to B, and add in extra ones" in {
case class CompleteRoot1(value1: Int, value: IncompleteValue)
case class IncompleteValue(v1: String)
case class CompleteValue(v1: String, v2: Int)
case class CompleteRoot2(value1: Int, value: CompleteValue)
val root1 = CompleteRoot1(42, IncompleteValue("forty two"))
implicit val ivToCv: Morphism[IncompleteValue, CompleteValue] = Morphism { iv =>
CompleteValue(
v1 = iv.v1,
v2 = 9138257
)
}
val root2 = root1.morphTo[CompleteRoot2]
assertResult(CompleteRoot2(value1 = 42, value = CompleteValue("forty two", 9138257)))(root2)
}
/* ==========================================================================
========================================================================== */
behavior of "Shapeless morph (Deep case-classes with sealed trait hierarchies)"
it should "... copy all properties from A to B, and morph sealed trait as well" in {
case class Root1(value1: Int, h: Hierarchy1)
sealed trait Hierarchy1
object Hierarchy1 {
case class Value1(i: Int) extends Hierarchy1
case class Value2(s: String) extends Hierarchy1
case class Value3(t: TestV) extends Hierarchy1
case class TestV(
v1: String,
v2: Int
)
}
case class Root2(value1: Int, h: Hierarchy2)
sealed trait Hierarchy2
object Hierarchy2 {
case class Value1(i: Int) extends Hierarchy2
case class Value2(s: String) extends Hierarchy2
case class Value3(t: TestV) extends Hierarchy2
case class TestV(
v1: String,
v2: Int
)
}
implicit val hierarchyMorph: Morphism[Hierarchy1, Hierarchy2] = Morphism {
case e: Hierarchy1.Value1 => e.morphTo[Hierarchy2.Value1]
case e: Hierarchy1.Value2 => e.morphTo[Hierarchy2.Value2]
case e: Hierarchy1.Value3 => e.morphTo[Hierarchy2.Value3]
}
withClue("... morph Value1") {
val rWithV1 = Root1(
value1 = 42,
h = Hierarchy1.Value1(11)
)
val rWithV1Morphed = rWithV1.morphTo[Root2]
assertResult(Root2(42, Hierarchy2.Value1(11)))(rWithV1Morphed)
}
withClue("... morph Value2") {
val rWithV2 = Root1(
value1 = 42,
h = Hierarchy1.Value2("12433")
)
val rWithV2Morphed = rWithV2.morphTo[Root2]
assertResult(Root2(42, Hierarchy2.Value2("12433")))(rWithV2Morphed)
}
withClue("... morph Value3") {
val rWithV3 = Root1(
value1 = 42,
h = Hierarchy1.Value3(Hierarchy1.TestV("asdfsdf", 42))
)
val rWithV3Morphed = rWithV3.morphTo[Root2]
assertResult(Root2(42, Hierarchy2.Value3(Hierarchy2.TestV("asdfsdf", 42))))(rWithV3Morphed)
}
}
/* ==========================================================================
========================================================================== */
behavior of "Shapeless morph (With field overrides)"
it should "... copy all properties from A to B and apply overrides" in {
val one = One("v1", "v2", 3)
val two = one.morphMerge[Two](
'value3 ->> 4 :: HNil
)
(two.value1, two.value2, two.value3) shouldEqual ((one.value1, one.value2, 4))
}
/* ==========================================================================
========================================================================== */
behavior of "Shapeless morph (Add absent fields)"
it should "... copy all properties from A to B and add field" in {
case class Source(field1: String)
case class Target(field1: String, field2: Int)
val source = Source("1")
val target = source.morphMerge[Target](
'field2 ->> 9 :: HNil
)
(target.field1, target.field2) shouldEqual ((source.field1, 9))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment