Skip to content
This repository was archived by the owner on Jan 24, 2025. It is now read-only.

Commit 5b3648a

Browse files
committed
Use scala macros to reduce number of methods for objectMatcher replacement
1 parent dc0595d commit 5b3648a

File tree

8 files changed

+107
-55
lines changed

8 files changed

+107
-55
lines changed

core/src/main/scala/com/softwaremill/diffx/Diff.scala

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -101,25 +101,13 @@ case class DiffLens[T, U](outer: Diff[T], path: List[String]) {
101101
}
102102
def ignore(): Diff[T] = outer.modifyUnsafe(path: _*)(Diff.ignored)
103103

104-
def withMapMatcher[K, V](m: ObjectMatcher[MapEntry[K, V]])(implicit ev1: U <:< scala.collection.Map[K, V]): Diff[T] =
105-
outer.modifyMatcherUnsafe(path: _*)(m)
106-
def withSetMatcher[V](m: ObjectMatcher[V])(implicit ev2: U <:< scala.collection.Set[V]): Diff[T] =
107-
outer.modifyMatcherUnsafe(path: _*)(m)
108-
def withListMatcher[V](m: ObjectMatcher[IterableEntry[V]])(implicit ev3: U <:< Iterable[V]): Diff[T] =
109-
outer.modifyMatcherUnsafe(path: _*)(m)
104+
def useMatcher[M](matcher: ObjectMatcher[M]): Diff[T] = macro ModifyMacro.withObjectMatcher[T, U, M]
110105
}
111106
case class DerivedDiffLens[T, U](outer: Diff[T], path: List[String]) {
112107
def setTo(d: Diff[U]): Derived[Diff[T]] = {
113108
Derived(outer.modifyUnsafe(path: _*)(d))
114109
}
115110
def ignore(): Derived[Diff[T]] = Derived(outer.modifyUnsafe(path: _*)(Diff.ignored))
116111

117-
def withMapMatcher[K, V](m: ObjectMatcher[MapEntry[K, V]])(implicit
118-
ev1: U <:< scala.collection.Map[K, V]
119-
): Derived[Diff[T]] =
120-
Derived(outer.modifyMatcherUnsafe(path: _*)(m))
121-
def withSetMatcher[V](m: ObjectMatcher[V])(implicit ev2: U <:< scala.collection.Set[V]): Derived[Diff[T]] =
122-
Derived(outer.modifyMatcherUnsafe(path: _*)(m))
123-
def withListMatcher[V](m: ObjectMatcher[IterableEntry[V]])(implicit ev3: U <:< Iterable[V]): Derived[Diff[T]] =
124-
Derived(outer.modifyMatcherUnsafe(path: _*)(m))
112+
def useMatcher[M](matcher: ObjectMatcher[M]): Derived[Diff[T]] = macro ModifyMacro.withObjectMatcherDerived[T, U, M]
125113
}

core/src/main/scala/com/softwaremill/diffx/ModifyMacro.scala

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,4 +110,36 @@ object ModifyMacro {
110110

111111
private[diffx] def modifiedFromPath[T, U](path: T => U): List[String] =
112112
macro modifiedFromPathMacro[T, U]
113+
114+
def withObjectMatcherDerived[T: c.WeakTypeTag, U: c.WeakTypeTag, M: c.WeakTypeTag](
115+
c: blackbox.Context
116+
)(matcher: c.Expr[ObjectMatcher[M]]): c.Tree = {
117+
import c.universe._
118+
val diff = withObjectMatcher[T, U, M](c)(matcher)
119+
q"com.softwaremill.diffx.Derived($diff)"
120+
}
121+
122+
def withObjectMatcher[T: c.WeakTypeTag, U: c.WeakTypeTag, M: c.WeakTypeTag](
123+
c: blackbox.Context
124+
)(matcher: c.Expr[ObjectMatcher[M]]): c.Tree = {
125+
import c.universe._
126+
val t = weakTypeOf[T]
127+
val u = weakTypeOf[U]
128+
val m = weakTypeOf[M]
129+
130+
val baseIsIterable = u <:< typeOf[Iterable[_]]
131+
val baseIsSet = u <:< typeOf[Set[_]]
132+
val baseIsMap = u <:< typeOf[Map[_, _]]
133+
val typeArgsTheSame = u.typeArgs == m.typeArgs
134+
val setRequirements = baseIsSet && u.typeArgs == List(m)
135+
val iterableRequirements = !baseIsSet && baseIsIterable && typeArgsTheSame
136+
val mapRequirements = baseIsMap && typeArgsTheSame
137+
if (!setRequirements && !iterableRequirements && !mapRequirements) { // weakTypeOf[U] <:< tq"Iterable[${u.typeArgs.head.termSymbol}]"
138+
c.abort(c.enclosingPosition, s"Invalid objectMather type $u for given lens($t,$m)")
139+
}
140+
q"""
141+
val lens = ${c.prefix}
142+
lens.outer.modifyMatcherUnsafe(lens.path: _*)($matcher)
143+
"""
144+
}
113145
}

core/src/main/scala/com/softwaremill/diffx/ObjectMatcher.scala

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,9 @@ trait ObjectMatcher[T] {
1010
object ObjectMatcher extends LowPriorityObjectMatcher {
1111
def apply[T: ObjectMatcher]: ObjectMatcher[T] = implicitly[ObjectMatcher[T]]
1212

13-
/** Given product of type T and its property U, match that products using U's objectMatcher */
14-
def by[T, U: ObjectMatcher](f: T => U): ObjectMatcher[T] = (left: T, right: T) =>
13+
private def by[T, U: ObjectMatcher](f: T => U): ObjectMatcher[T] = (left: T, right: T) =>
1514
ObjectMatcher[U].isSameObject(f(left), f(right))
1615

17-
/** Given key-value (K,V) pairs match them using V's objectMatcher */
18-
def byValue[K, V: ObjectMatcher]: ObjectMatcher[MapEntry[K, V]] = ObjectMatcher.by[MapEntry[K, V], V](_.value)
19-
20-
/** Given key-value (K,V) pairs, where V is a type of product and U is a property of V, match them using U's objectMatcher */
21-
def byValue[K, V, U: ObjectMatcher](f: V => U): ObjectMatcher[MapEntry[K, V]] =
22-
ObjectMatcher.byValue[K, V](ObjectMatcher.by[V, U](f))
23-
2416
implicit def optionMatcher[T: ObjectMatcher]: ObjectMatcher[Option[T]] = (left: Option[T], right: Option[T]) => {
2517
(left, right) match {
2618
case (Some(l), Some(r)) => ObjectMatcher[T].isSameObject(l, r)
@@ -33,6 +25,34 @@ object ObjectMatcher extends LowPriorityObjectMatcher {
3325

3426
type IterableEntry[T] = MapEntry[Int, T]
3527
case class MapEntry[K, V](key: K, value: V)
28+
29+
def list[T] = new ObjectMatcherListHelper[T]
30+
31+
class ObjectMatcherListHelper[V] {
32+
def byValue[U: ObjectMatcher](f: V => U): ObjectMatcher[IterableEntry[V]] = byValue(ObjectMatcher.by[V, U](f))
33+
def byValue(implicit ev: ObjectMatcher[V]): ObjectMatcher[IterableEntry[V]] =
34+
ObjectMatcher.by[IterableEntry[V], V](_.value)
35+
36+
def byKey[U: ObjectMatcher](f: Int => U): ObjectMatcher[IterableEntry[V]] = byKey(ObjectMatcher.by(f))
37+
def byKey(implicit ko: ObjectMatcher[Int]): ObjectMatcher[IterableEntry[V]] = ObjectMatcher.by(_.key)
38+
}
39+
40+
def set[T] = new ObjectMatcherSetHelper[T]
41+
42+
class ObjectMatcherSetHelper[T] {
43+
def by[U: ObjectMatcher](f: T => U): ObjectMatcher[T] = (left: T, right: T) =>
44+
ObjectMatcher[U].isSameObject(f(left), f(right))
45+
}
46+
47+
def map[K, V] = new ObjectMatcherMapHelper[K, V]
48+
49+
class ObjectMatcherMapHelper[K, V] {
50+
def byValue[U: ObjectMatcher](f: V => U): ObjectMatcher[MapEntry[K, V]] = byValue(ObjectMatcher.by(f))
51+
def byValue(implicit ev: ObjectMatcher[V]): ObjectMatcher[MapEntry[K, V]] = ObjectMatcher.by(_.value)
52+
53+
def byKey[U: ObjectMatcher](f: K => U): ObjectMatcher[MapEntry[K, V]] = byKey(ObjectMatcher.by(f))
54+
def byKey(implicit ko: ObjectMatcher[K]): ObjectMatcher[MapEntry[K, V]] = ObjectMatcher.by(_.key)
55+
}
3656
}
3757

3858
trait LowPriorityObjectMatcher {

core/src/test/scala/com/softwaremill/diffx/test/DiffModifyIntegrationTest.scala

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package com.softwaremill.diffx.test
22

3-
import com.softwaremill.diffx.ObjectMatcher.IterableEntry
43
import com.softwaremill.diffx._
54
import org.scalatest.flatspec.AnyFlatSpec
65
import org.scalatest.matchers.should.Matchers
@@ -42,8 +41,8 @@ class DiffModifyIntegrationTest extends AnyFlatSpec with Matchers {
4241
val o2 = Organization(List(p2, p1))
4342
implicit val orgDiff: Diff[Organization] = Derived[Diff[Organization]]
4443
.modify(_.people)
45-
.withListMatcher(
46-
ObjectMatcher.byValue[Int, Person](ObjectMatcher.by(_.name))
44+
.useMatcher(
45+
ObjectMatcher.list[Person].byValue(_.name)
4746
)
4847
compare(o1, o2).isIdentical shouldBe true
4948
}
@@ -80,8 +79,8 @@ class DiffModifyIntegrationTest extends AnyFlatSpec with Matchers {
8079
it should "match map entries by values" in {
8180
implicit val lookupDiff: Diff[MyLookup] = Derived[Diff[MyLookup]]
8281
.modify(_.map)
83-
.withMapMatcher(
84-
ObjectMatcher.byValue[KeyModel, String]
82+
.useMatcher(
83+
ObjectMatcher.map[KeyModel, String].byValue
8584
)
8685
val uuid1 = UUID.randomUUID()
8786
val uuid2 = UUID.randomUUID()
@@ -108,7 +107,7 @@ class DiffModifyIntegrationTest extends AnyFlatSpec with Matchers {
108107
it should "use overrided object matcher when comparing set" in {
109108
implicit val lookupDiff: Diff[Startup] = Derived[Diff[Startup]]
110109
.modify(_.workers)
111-
.withSetMatcher[Person](ObjectMatcher.by(_.name))
110+
.useMatcher(ObjectMatcher.set[Person].by(_.name))
112111
val p2m = p2.copy(age = 33)
113112
compare(Startup(Set(p1, p2)), Startup(Set(p1, p2m))) shouldBe DiffResultObject(
114113
"Startup",
@@ -140,8 +139,9 @@ class DiffModifyIntegrationTest extends AnyFlatSpec with Matchers {
140139
it should "compare lists using object matcher comparator passed explicitly" in {
141140
val o1 = Organization(List(p1, p2))
142141
val o2 = Organization(List(p2, p1))
143-
val om: ObjectMatcher[IterableEntry[Person]] = ObjectMatcher.byValue(_.name)
144-
val d = Diff[Organization].modify(_.people).withListMatcher(om)
142+
val d = Diff[Organization]
143+
.modify(_.people)
144+
.useMatcher(ObjectMatcher.list[Person].byValue(_.name))
145145
compare(o1, o2)(d).isIdentical shouldBe true
146146
}
147147
}

core/src/test/scala/com/softwaremill/diffx/test/DiffTest.scala

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -390,47 +390,46 @@ class DiffTest extends AnyFreeSpec with Matchers {
390390
"compare lists using set-like comparator" in {
391391
val o1 = Organization(List(p1, p2))
392392
val o2 = Organization(List(p2, p1))
393-
implicit val om: ObjectMatcher[Person] = ObjectMatcher.by(_.name)
393+
implicit val om = ObjectMatcher.set[Person].by(_.name)
394394
implicit val dd: Diff[List[Person]] = Diff[Set[Person]].contramap(_.toSet)
395395
compare(o1, o2).isIdentical shouldBe true
396396
}
397397

398398
"compare lists using object matcher comparator" in {
399399
val o1 = Organization(List(p1, p2))
400400
val o2 = Organization(List(p2, p1))
401-
implicit val om: ObjectMatcher[IterableEntry[Person]] = ObjectMatcher.byValue(_.name)
401+
implicit val om = ObjectMatcher.list[Person].byValue(_.name)
402402
compare(o1, o2).isIdentical shouldBe true
403403
}
404404

405405
"compare lists using object matcher comparator when matching by pair" in {
406406
val p2WithSameNameAsP1 = p2.copy(name = p1.name)
407407
val o1 = Organization(List(p1, p2WithSameNameAsP1))
408408
val o2 = Organization(List(p2WithSameNameAsP1, p1))
409-
implicit val om: ObjectMatcher[IterableEntry[Person]] = ObjectMatcher.byValue(p => (p.name, p.age))
409+
implicit val om = ObjectMatcher.list[Person].byValue(p => (p.name, p.age))
410410
compare(o1, o2).isIdentical shouldBe true
411411
}
412412

413413
"compare lists using explicit object matcher comparator" in {
414414
val o1 = Organization(List(p1, p2))
415415
val o2 = Organization(List(p2, p1))
416-
implicit val orgDiff: Diff[Organization] = Derived[Diff[Organization]].modifyMatcherUnsafe("people")(
417-
ObjectMatcher.byValue[Int, Person](ObjectMatcher.by(_.name))
418-
)
416+
implicit val orgDiff: Diff[Organization] = Derived[Diff[Organization]]
417+
.modifyMatcherUnsafe("people")(ObjectMatcher.list[Person].byValue(_.name))
419418
compare(o1, o2).isIdentical shouldBe true
420419
}
421420

422421
"compare lists using value object matcher" in {
423422
val p2WithSameNameAsP1 = p2.copy(name = p1.name)
424423
val o1 = Organization(List(p1, p2WithSameNameAsP1))
425424
val o2 = Organization(List(p2WithSameNameAsP1, p1))
426-
implicit val om: ObjectMatcher[IterableEntry[Person]] = ObjectMatcher.byValue(identity(_))
425+
implicit val om = ObjectMatcher.list[Person].byValue(identity(_))
427426
compare(o1, o2).isIdentical shouldBe true
428427
}
429428

430429
"compare correctly lists with duplicates using objectMatcher" in {
431430
val o1 = Organization(List(p1, p1))
432431
val o2 = Organization(List(p1, p1))
433-
implicit val om: ObjectMatcher[IterableEntry[Person]] = ObjectMatcher.byValue(identity(_))
432+
implicit val om = ObjectMatcher.list[Person].byValue(identity(_))
434433
val result = compare(o1, o2)
435434
result.isIdentical shouldBe true
436435
}
@@ -489,7 +488,7 @@ class DiffTest extends AnyFreeSpec with Matchers {
489488
"ignored fields from elements" in {
490489
val p2m = p2.copy(age = 33, in = Instant.now())
491490
implicit val d: Diff[Person] = Derived[Diff[Person]].modifyUnsafe("age")(Diff.ignored)
492-
implicit val im: ObjectMatcher[Person] = ObjectMatcher.by(_.name)
491+
implicit val im: ObjectMatcher[Person] = ObjectMatcher.set.by(_.name)
493492
compare(Set(p1, p2), Set(p1, p2m)) shouldBe DiffResultSet(
494493
List(
495494
DiffResultObject(
@@ -534,7 +533,7 @@ class DiffTest extends AnyFreeSpec with Matchers {
534533

535534
"propagate ignore fields to elements" in {
536535
val p2m = p2.copy(in = Instant.now())
537-
implicit val im: ObjectMatcher[Person] = ObjectMatcher.by(_.name)
536+
implicit val im: ObjectMatcher[Person] = ObjectMatcher.set.by(_.name)
538537
implicit val ds: Diff[Person] = Derived[Diff[Person]].modifyUnsafe("age")(Diff.ignored)
539538
compare(Set(p1, p2), Set(p1, p2m)) shouldBe DiffResultSet(
540539
List(
@@ -583,7 +582,7 @@ class DiffTest extends AnyFreeSpec with Matchers {
583582

584583
"set of products using instance matcher" in {
585584
val p2m = p2.copy(age = 33)
586-
implicit val im: ObjectMatcher[Person] = ObjectMatcher.by(_.name)
585+
implicit val im: ObjectMatcher[Person] = ObjectMatcher.set.by(_.name)
587586
compare(Startup(Set(p1, p2)), Startup(Set(p1, p2m))) shouldBe DiffResultObject(
588587
"Startup",
589588
Map(
@@ -683,7 +682,7 @@ class DiffTest extends AnyFreeSpec with Matchers {
683682
}
684683

685684
"match keys using object mapper" in {
686-
implicit val om: ObjectMatcher[KeyModel] = ObjectMatcher.by(_.name)
685+
implicit val om: ObjectMatcher[KeyModel] = ObjectMatcher.set.by(_.name)
687686
val uuid1 = UUID.randomUUID()
688687
val uuid2 = UUID.randomUUID()
689688
val a1 = MyLookup(Map(KeyModel(uuid1, "k1") -> "val1"))
@@ -707,7 +706,7 @@ class DiffTest extends AnyFreeSpec with Matchers {
707706
}
708707

709708
"match map entries by values" in {
710-
implicit val om: ObjectMatcher[MapEntry[KeyModel, String]] = ObjectMatcher.byValue
709+
implicit val om: ObjectMatcher[MapEntry[KeyModel, String]] = ObjectMatcher.map.byValue
711710
val uuid1 = UUID.randomUUID()
712711
val uuid2 = UUID.randomUUID()
713712
val a1 = MyLookup(Map(KeyModel(uuid1, "k1") -> "val1"))
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.softwaremill.diffx.test
2+
3+
import com.softwaremill.diffx.generic.auto._
4+
import com.softwaremill.diffx.{Diff, ObjectMatcher}
5+
import org.scalatest.freespec.AnyFreeSpec
6+
7+
class MyTest extends AnyFreeSpec {
8+
case class Organization(people: Set[Person])
9+
10+
"asd" in {
11+
Diff[Organization].modify(_.people).useMatcher(ObjectMatcher.set[Person].by(_.name))
12+
// Diff[Organization].modify(_.people).withListMatcher(ObjectMatcher.byValue[Int, Person](_.name))
13+
}
14+
}

docs-sources/usage/replacing.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,10 @@ In fact, replacement is so powerful that ignoring is implemented as a replacemen
3030
with the `Diff.ignore` instance.
3131

3232
You can use the same mechanism to set particular object matcher for given nested collection in the hierarchy.
33-
Depending, whether it is list, set or map a respective method needs to be called:
3433
```scala mdoc:silent
3534
case class Organization(peopleList: List[Person], peopleSet: Set[Person], peopleMap: Map[String, Person])
3635
implicit val diffOrg: Derived[Diff[Organization]] = Diff.derived[Organization]
37-
.modify(_.peopleList).withListMatcher[Person](ObjectMatcher.byValue(_.age))
38-
.modify(_.peopleSet).withSetMatcher[Person](ObjectMatcher.by(_.age))
39-
.modify(_.peopleMap).withMapMatcher[String,Person](ObjectMatcher.byValue(_.age))
36+
.modify(_.peopleList).useMatcher(ObjectMatcher.list[Person].byValue(_.age))
37+
.modify(_.peopleSet).useMatcher(ObjectMatcher.set[Person].by(_.age))
38+
.modify(_.peopleMap).useMatcher(ObjectMatcher.map[String, Person].byValue(_.age))
4039
```

docs-sources/usage/sequences.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
`diffx` provides instances for many containers from scala's standard library (e.g. lists, sets, maps), however
44
not all collections can be simply compared. Ordered collections like lists or vectors are compared by default by
55
comparing elements under the same indexes.
6-
Maps, by default, are compared by comparing values under the respective keys.
6+
On the other hand maps, by default, are compared by comparing values under the respective keys.
77
For unordered collections there is an `ObjectMapper` typeclass which defines how elements should be paired.
88

99
## object matcher
@@ -20,9 +20,10 @@ It is mostly useful when comparing unordered collections like sets:
2020
```scala mdoc:silent
2121
import com.softwaremill.diffx._
2222
import com.softwaremill.diffx.generic.auto._
23+
2324
case class Person(id: String, name: String)
2425

25-
implicit val personMatcher: ObjectMatcher[Person] = ObjectMatcher.by(_.id)
26+
implicit val personMatcher: ObjectMatcher[Person] = ObjectMatcher.set.by(_.id)
2627
val bob = Person("1","Bob")
2728
```
2829
```scala mdoc
@@ -34,11 +35,10 @@ In below example we tell `diffx` to compare these maps by paring entries by valu
3435
```scala mdoc:reset:silent
3536
import com.softwaremill.diffx._
3637
import com.softwaremill.diffx.generic.auto._
37-
import com.softwaremill.diffx.ObjectMatcher.MapEntry
38+
3839
case class Person(id: String, name: String)
3940

40-
val personMatcher: ObjectMatcher[Person] = ObjectMatcher.by(_.id)
41-
implicit val om: ObjectMatcher[MapEntry[String, Person]] = ObjectMatcher.byValue(personMatcher)
41+
implicit val om = ObjectMatcher.map[String, Person].byValue(_.id)
4242
val bob = Person("1","Bob")
4343
```
4444

@@ -53,10 +53,10 @@ but the key type is bound to `Int` (`IterableEntry` is an alias for `MapEntry[In
5353
```scala mdoc:reset:silent
5454
import com.softwaremill.diffx._
5555
import com.softwaremill.diffx.generic.auto._
56-
import com.softwaremill.diffx.ObjectMatcher.IterableEntry
56+
5757
case class Person(id: String, name: String)
5858

59-
implicit val personMatcher: ObjectMatcher[IterableEntry[Person]] = ObjectMatcher.byValue(_.id)
59+
implicit val personMatcher = ObjectMatcher.list[Person].byValue(_.id)
6060
val bob = Person("1","Bob")
6161
val alice = Person("2","Alice")
6262
```

0 commit comments

Comments
 (0)