Created
November 25, 2016 12:06
-
-
Save davydkov/1f8d1fdc24adc25d15a29655a89a6879 to your computer and use it in GitHub Desktop.
Scala morphism with shapeless
This file contains 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 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) | |
} | |
} |
This file contains 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
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