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

Commit 2ddac26

Browse files
committed
Use scala macros to reduce number of methods for objectMatcher replacement
1 parent 43e5f2c commit 2ddac26

File tree

8 files changed

+133
-55
lines changed

8 files changed

+133
-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
@@ -112,12 +112,7 @@ case class DiffLens[T, U](outer: Diff[T], path: List[String]) {
112112

113113
def ignore(implicit config: DiffConfiguration): Diff[T] = outer.modifyUnsafe(path: _*)(config.makeIgnored)
114114

115-
def withMapMatcher[K, V](m: ObjectMatcher[MapEntry[K, V]])(implicit ev1: U <:< scala.collection.Map[K, V]): Diff[T] =
116-
outer.modifyMatcherUnsafe(path: _*)(m)
117-
def withSetMatcher[V](m: ObjectMatcher[V])(implicit ev2: U <:< scala.collection.Set[V]): Diff[T] =
118-
outer.modifyMatcherUnsafe(path: _*)(m)
119-
def withListMatcher[V](m: ObjectMatcher[IterableEntry[V]])(implicit ev3: U <:< Iterable[V]): Diff[T] =
120-
outer.modifyMatcherUnsafe(path: _*)(m)
115+
def useMatcher[M](matcher: ObjectMatcher[M]): Diff[T] = macro ModifyMacro.withObjectMatcher[T, U, M]
121116
}
122117
case class DerivedDiffLens[T, U](outer: Diff[T], path: List[String]) {
123118
def setTo(d: Diff[U]): Derived[Diff[T]] = using(_ => d)
@@ -130,12 +125,5 @@ case class DerivedDiffLens[T, U](outer: Diff[T], path: List[String]) {
130125
outer.modifyUnsafe(path: _*)(config.makeIgnored)
131126
)
132127

133-
def withMapMatcher[K, V](m: ObjectMatcher[MapEntry[K, V]])(implicit
134-
ev1: U <:< scala.collection.Map[K, V]
135-
): Derived[Diff[T]] =
136-
Derived(outer.modifyMatcherUnsafe(path: _*)(m))
137-
def withSetMatcher[V](m: ObjectMatcher[V])(implicit ev2: U <:< scala.collection.Set[V]): Derived[Diff[T]] =
138-
Derived(outer.modifyMatcherUnsafe(path: _*)(m))
139-
def withListMatcher[V](m: ObjectMatcher[IterableEntry[V]])(implicit ev3: U <:< Iterable[V]): Derived[Diff[T]] =
140-
Derived(outer.modifyMatcherUnsafe(path: _*)(m))
128+
def useMatcher[M](matcher: ObjectMatcher[M]): Derived[Diff[T]] = macro ModifyMacro.withObjectMatcherDerived[T, U, M]
141129
}

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

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

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

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 MapEntry[K,V] 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 MapEntry[K,V], 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
@@ -393,47 +393,46 @@ class DiffTest extends AnyFreeSpec with Matchers {
393393
"compare lists using set-like comparator" in {
394394
val o1 = Organization(List(p1, p2))
395395
val o2 = Organization(List(p2, p1))
396-
implicit val om: ObjectMatcher[Person] = ObjectMatcher.by(_.name)
396+
implicit val om = ObjectMatcher.set[Person].by(_.name)
397397
implicit val dd: Diff[List[Person]] = Diff[Set[Person]].contramap(_.toSet)
398398
compare(o1, o2).isIdentical shouldBe true
399399
}
400400

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

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

416416
"compare lists using explicit object matcher comparator" in {
417417
val o1 = Organization(List(p1, p2))
418418
val o2 = Organization(List(p2, p1))
419-
implicit val orgDiff: Diff[Organization] = Derived[Diff[Organization]].modifyMatcherUnsafe("people")(
420-
ObjectMatcher.byValue[Int, Person](ObjectMatcher.by(_.name))
421-
)
419+
implicit val orgDiff: Diff[Organization] = Derived[Diff[Organization]]
420+
.modifyMatcherUnsafe("people")(ObjectMatcher.list[Person].byValue(_.name))
422421
compare(o1, o2).isIdentical shouldBe true
423422
}
424423

425424
"compare lists using value object matcher" in {
426425
val p2WithSameNameAsP1 = p2.copy(name = p1.name)
427426
val o1 = Organization(List(p1, p2WithSameNameAsP1))
428427
val o2 = Organization(List(p2WithSameNameAsP1, p1))
429-
implicit val om: ObjectMatcher[IterableEntry[Person]] = ObjectMatcher.byValue(identity(_))
428+
implicit val om = ObjectMatcher.list[Person].byValue(identity(_))
430429
compare(o1, o2).isIdentical shouldBe true
431430
}
432431

433432
"compare correctly lists with duplicates using objectMatcher" in {
434433
val o1 = Organization(List(p1, p1))
435434
val o2 = Organization(List(p1, p1))
436-
implicit val om: ObjectMatcher[IterableEntry[Person]] = ObjectMatcher.byValue(identity(_))
435+
implicit val om = ObjectMatcher.list[Person].byValue(identity(_))
437436
val result = compare(o1, o2)
438437
result.isIdentical shouldBe true
439438
}
@@ -492,7 +491,7 @@ class DiffTest extends AnyFreeSpec with Matchers {
492491
"ignored fields from elements" in {
493492
val p2m = p2.copy(age = 33, in = Instant.now())
494493
implicit val d: Diff[Person] = Derived[Diff[Person]].modifyUnsafe("age")(ignored)
495-
implicit val im: ObjectMatcher[Person] = ObjectMatcher.by(_.name)
494+
implicit val im: ObjectMatcher[Person] = ObjectMatcher.set.by(_.name)
496495
compare(Set(p1, p2), Set(p1, p2m)) shouldBe DiffResultSet(
497496
List(
498497
DiffResultObject(
@@ -537,7 +536,7 @@ class DiffTest extends AnyFreeSpec with Matchers {
537536

538537
"propagate ignore fields to elements" in {
539538
val p2m = p2.copy(in = Instant.now())
540-
implicit val im: ObjectMatcher[Person] = ObjectMatcher.by(_.name)
539+
implicit val im: ObjectMatcher[Person] = ObjectMatcher.set.by(_.name)
541540
implicit val ds: Diff[Person] = Derived[Diff[Person]].modifyUnsafe("age")(ignored)
542541
compare(Set(p1, p2), Set(p1, p2m)) shouldBe DiffResultSet(
543542
List(
@@ -586,7 +585,7 @@ class DiffTest extends AnyFreeSpec with Matchers {
586585

587586
"set of products using instance matcher" in {
588587
val p2m = p2.copy(age = 33)
589-
implicit val im: ObjectMatcher[Person] = ObjectMatcher.by(_.name)
588+
implicit val im: ObjectMatcher[Person] = ObjectMatcher.set.by(_.name)
590589
compare(Startup(Set(p1, p2)), Startup(Set(p1, p2m))) shouldBe DiffResultObject(
591590
"Startup",
592591
Map(
@@ -686,7 +685,7 @@ class DiffTest extends AnyFreeSpec with Matchers {
686685
}
687686

688687
"match keys using object mapper" in {
689-
implicit val om: ObjectMatcher[KeyModel] = ObjectMatcher.by(_.name)
688+
implicit val om: ObjectMatcher[KeyModel] = ObjectMatcher.set.by(_.name)
690689
val uuid1 = UUID.randomUUID()
691690
val uuid2 = UUID.randomUUID()
692691
val a1 = MyLookup(Map(KeyModel(uuid1, "k1") -> "val1"))
@@ -710,7 +709,7 @@ class DiffTest extends AnyFreeSpec with Matchers {
710709
}
711710

712711
"match map entries by values" in {
713-
implicit val om: ObjectMatcher[MapEntry[KeyModel, String]] = ObjectMatcher.byValue
712+
implicit val om: ObjectMatcher[MapEntry[KeyModel, String]] = ObjectMatcher.map.byValue
714713
val uuid1 = UUID.randomUUID()
715714
val uuid2 = UUID.randomUUID()
716715
val a1 = MyLookup(Map(KeyModel(uuid1, "k1") -> "val1"))
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.softwaremill.diffx.test
2+
3+
import org.scalatest.freespec.AnyFreeSpec
4+
5+
class UseMatcherMacroTest extends AnyFreeSpec {
6+
case class Organization(people: Set[Person])
7+
8+
"should compile when using with set matcher" in {
9+
assertCompiles("""import com.softwaremill.diffx.generic.auto._
10+
|import com.softwaremill.diffx.{Diff, ObjectMatcher}
11+
|
12+
|case class Organization(people: Set[Person])
13+
|case class Person(name: String, age: Int)
14+
|
15+
|Diff[Organization].modify(_.people).useMatcher(ObjectMatcher.set[Person].by(_.name))
16+
|""".stripMargin)
17+
}
18+
19+
"should compile when using with list matcher" in {
20+
assertCompiles("""import com.softwaremill.diffx.generic.auto._
21+
|import com.softwaremill.diffx.{Diff, ObjectMatcher}
22+
|
23+
|case class Organization(people: List[Person])
24+
|case class Person(name: String, age: Int)
25+
|
26+
|Diff[Organization].modify(_.people).useMatcher(ObjectMatcher.list[Person].byValue(_.name))
27+
|""".stripMargin)
28+
}
29+
30+
"should compile when using with map matcher" in {
31+
assertCompiles("""import com.softwaremill.diffx.generic.auto._
32+
|import com.softwaremill.diffx.{Diff, ObjectMatcher}
33+
|
34+
|case class Organization(people: Map[String, Person])
35+
|case class Person(name: String, age: Int)
36+
|
37+
|Diff[Organization].modify(_.people).useMatcher(ObjectMatcher.map[String, Person].byValue(_.name))
38+
|""".stripMargin)
39+
}
40+
}

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)