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

Commit 43e5f2c

Browse files
Ignore customization (#281)
* Allow overriding how ignored diffs are produced * Rename modify to using on lenses
1 parent 13aa25a commit 43e5f2c

File tree

12 files changed

+139
-43
lines changed

12 files changed

+139
-43
lines changed

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

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,16 @@ trait Diff[-T] { outer =>
1212
outer(f(left), f(right), context)
1313
}
1414

15-
def modifyUnsafe(path: String*)(diff: Diff[_]): Diff[T] =
15+
def modifyUnsafe[U](path: String*)(mod: Diff[U] => Diff[U]): Diff[T] =
1616
new Diff[T] {
1717
override def apply(left: T, right: T, context: DiffContext): DiffResult =
18-
outer.apply(left, right, context.merge(DiffContext(Tree.fromList(path.toList, diff), List.empty, Tree.empty)))
18+
outer.apply(
19+
left,
20+
right,
21+
context.merge(
22+
DiffContext.atPath(path.toList, mod.asInstanceOf[Diff[Any] => Diff[Any]])
23+
)
24+
)
1925
}
2026

2127
def modifyMatcherUnsafe(path: String*)(matcher: ObjectMatcher[_]): Diff[T] =
@@ -24,7 +30,7 @@ trait Diff[-T] { outer =>
2430
outer.apply(
2531
left,
2632
right,
27-
context.merge(DiffContext(Tree.empty, List.empty, Tree.fromList(path.toList, matcher)))
33+
context.merge(DiffContext.atPath(path.toList, matcher))
2834
)
2935
}
3036
}
@@ -79,13 +85,15 @@ trait LowPriorityDiff {
7985
implicit class RichDerivedDiff[T](val dd: Derived[Diff[T]]) {
8086
def contramap[R](f: R => T): Derived[Diff[R]] = Derived(dd.value.contramap(f))
8187

82-
def modify[U](path: T => U): DerivedDiffLens[T, U] = macro ModifyMacro.derivedModifyMacro[T, U]
83-
def ignore[U](path: T => U): Derived[Diff[T]] = macro ModifyMacro.derivedIgnoreMacro[T, U]
88+
def modify[U](path: T => U): DerivedDiffLens[T, U] =
89+
macro ModifyMacro.derivedModifyMacro[T, U]
90+
def ignore[U](path: T => U)(implicit conf: DiffConfiguration): Derived[Diff[T]] =
91+
macro ModifyMacro.derivedIgnoreMacro[T, U]
8492
}
8593

8694
implicit class RichDiff[T](val d: Diff[T]) {
8795
def modify[U](path: T => U): DiffLens[T, U] = macro ModifyMacro.modifyMacro[T, U]
88-
def ignore[U](path: T => U): Diff[T] = macro ModifyMacro.ignoreMacro[T, U]
96+
def ignore[U](path: T => U)(implicit conf: DiffConfiguration): Diff[T] = macro ModifyMacro.ignoreMacro[T, U]
8997
}
9098
}
9199

@@ -96,10 +104,13 @@ object Derived {
96104
}
97105

98106
case class DiffLens[T, U](outer: Diff[T], path: List[String]) {
99-
def setTo(d: Diff[U]): Diff[T] = {
100-
outer.modifyUnsafe(path: _*)(d)
107+
def setTo(d: Diff[U]): Diff[T] = using(_ => d)
108+
109+
def using(mod: Diff[U] => Diff[U]): Diff[T] = {
110+
outer.modifyUnsafe(path: _*)(mod)
101111
}
102-
def ignore(): Diff[T] = outer.modifyUnsafe(path: _*)(Diff.ignored)
112+
113+
def ignore(implicit config: DiffConfiguration): Diff[T] = outer.modifyUnsafe(path: _*)(config.makeIgnored)
103114

104115
def withMapMatcher[K, V](m: ObjectMatcher[MapEntry[K, V]])(implicit ev1: U <:< scala.collection.Map[K, V]): Diff[T] =
105116
outer.modifyMatcherUnsafe(path: _*)(m)
@@ -109,10 +120,15 @@ case class DiffLens[T, U](outer: Diff[T], path: List[String]) {
109120
outer.modifyMatcherUnsafe(path: _*)(m)
110121
}
111122
case class DerivedDiffLens[T, U](outer: Diff[T], path: List[String]) {
112-
def setTo(d: Diff[U]): Derived[Diff[T]] = {
113-
Derived(outer.modifyUnsafe(path: _*)(d))
123+
def setTo(d: Diff[U]): Derived[Diff[T]] = using(_ => d)
124+
125+
def using(mod: Diff[U] => Diff[U]): Derived[Diff[T]] = {
126+
Derived(outer.modifyUnsafe(path: _*)(mod))
114127
}
115-
def ignore(): Derived[Diff[T]] = Derived(outer.modifyUnsafe(path: _*)(Diff.ignored))
128+
129+
def ignore(implicit config: DiffConfiguration): Derived[Diff[T]] = Derived(
130+
outer.modifyUnsafe(path: _*)(config.makeIgnored)
131+
)
116132

117133
def withMapMatcher[K, V](m: ObjectMatcher[MapEntry[K, V]])(implicit
118134
ev1: U <:< scala.collection.Map[K, V]
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.softwaremill.diffx
2+
3+
case class DiffConfiguration(makeIgnored: Diff[Any] => Diff[Any])
4+
5+
object DiffConfiguration {
6+
implicit val Default: DiffConfiguration = DiffConfiguration(makeIgnored = (_: Diff[Any]) => Diff.ignored[Any])
7+
}

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

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

3-
case class DiffContext(overrides: Tree[Diff[_]], path: FieldPath, matcherOverrides: Tree[ObjectMatcher[_]]) {
3+
case class DiffContext(
4+
overrides: Tree[Diff[Any] => Diff[Any]],
5+
path: FieldPath,
6+
matcherOverrides: Tree[ObjectMatcher[_]]
7+
) {
48
def merge(other: DiffContext): DiffContext = {
59
DiffContext(overrides.merge(other.overrides), List.empty, matcherOverrides.merge(other.matcherOverrides))
610
}
711

8-
def getOverride(label: String): Option[Diff[_]] = {
12+
def getOverride(label: String): Option[Diff[Any] => Diff[Any]] = {
913
treeOverride(label, overrides)
1014
}
1115

@@ -50,7 +54,8 @@ case class DiffContext(overrides: Tree[Diff[_]], path: FieldPath, matcherOverrid
5054

5155
object DiffContext {
5256
val Empty: DiffContext = DiffContext(Tree.empty, List.empty, Tree.empty)
53-
def atPath(path: FieldPath, diff: Diff[_]): DiffContext = Empty.copy(overrides = Tree.fromList(path, diff))
57+
def atPath(path: FieldPath, mod: Diff[Any] => Diff[Any]): DiffContext =
58+
Empty.copy(overrides = Tree.fromList(path, mod))
5459
def atPath(path: FieldPath, matcher: ObjectMatcher[_]): DiffContext =
5560
Empty.copy(matcherOverrides = Tree.fromList(path, matcher))
5661
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ case class ConsoleColorConfig(
5959
)
6060

6161
object ConsoleColorConfig {
62+
val noColors: ConsoleColorConfig =
63+
ConsoleColorConfig(default = identity, arrow = identity, right = identity, left = identity)
6264
val dark: ConsoleColorConfig = ConsoleColorConfig(left = magenta, right = green, default = cyan, arrow = red)
6365
val light: ConsoleColorConfig = ConsoleColorConfig(default = black, arrow = red, left = magenta, right = blue)
6466
val normal: ConsoleColorConfig =

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

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,27 +20,29 @@ object ModifyMacro {
2020

2121
def derivedIgnoreMacro[T: c.WeakTypeTag, U: c.WeakTypeTag](
2222
c: blackbox.Context
23-
)(path: c.Expr[T => U]): c.Tree =
24-
applyIgnoredModified[T, U](c)(modifiedFromPathMacro(c)(path))
23+
)(path: c.Expr[T => U])(conf: c.Expr[DiffConfiguration]): c.Tree =
24+
applyIgnoredModified[T, U](c)(modifiedFromPathMacro(c)(path), conf)
2525

2626
private def applyIgnoredModified[T: c.WeakTypeTag, U: c.WeakTypeTag](c: blackbox.Context)(
27-
path: c.Expr[List[String]]
27+
path: c.Expr[List[String]],
28+
conf: c.Expr[DiffConfiguration]
2829
): c.Tree = {
2930
import c.universe._
3031
val lens = applyDerivedModified[T, U](c)(path)
31-
q"""$lens.ignore()"""
32+
q"""$lens.ignore($conf)"""
3233
}
3334

3435
def ignoreMacro[T: c.WeakTypeTag, U: c.WeakTypeTag](
3536
c: blackbox.Context
36-
)(path: c.Expr[T => U]): c.Tree = applyIgnored[T, U](c)(modifiedFromPathMacro(c)(path))
37+
)(path: c.Expr[T => U])(conf: c.Expr[DiffConfiguration]): c.Tree =
38+
applyIgnored[T, U](c)(modifiedFromPathMacro(c)(path), conf)
3739

3840
private def applyIgnored[T: c.WeakTypeTag, U: c.WeakTypeTag](
3941
c: blackbox.Context
40-
)(path: c.Expr[List[String]]): c.Tree = {
42+
)(path: c.Expr[List[String]], conf: c.Expr[DiffConfiguration]): c.Tree = {
4143
import c.universe._
4244
val lens = applyModified[T, U](c)(path)
43-
q"""$lens.ignore()"""
45+
q"""$lens.ignore($conf)"""
4446
}
4547

4648
def modifyMacro[T: c.WeakTypeTag, U: c.WeakTypeTag](

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ object ObjectMatcher extends LowPriorityObjectMatcher {
1414
def by[T, U: ObjectMatcher](f: T => U): ObjectMatcher[T] = (left: T, right: T) =>
1515
ObjectMatcher[U].isSameObject(f(left), f(right))
1616

17-
/** Given key-value (K,V) pairs match them using V's objectMatcher */
17+
/** Given MapEntry[K,V] match them using V's objectMatcher */
1818
def byValue[K, V: ObjectMatcher]: ObjectMatcher[MapEntry[K, V]] = ObjectMatcher.by[MapEntry[K, V], V](_.value)
1919

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 */
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 */
2121
def byValue[K, V, U: ObjectMatcher](f: V => U): ObjectMatcher[MapEntry[K, V]] =
2222
ObjectMatcher.byValue[K, V](ObjectMatcher.by[V, U](f))
2323

@@ -28,7 +28,7 @@ object ObjectMatcher extends LowPriorityObjectMatcher {
2828
}
2929
}
3030

31-
/** Given key-value (K,V) pairs, match them using K's objectMatcher */
31+
/** Given MapEntry[K,V], match them using K's objectMatcher */
3232
implicit def byKey[K: ObjectMatcher, V]: ObjectMatcher[MapEntry[K, V]] = ObjectMatcher.by[MapEntry[K, V], K](_.key)
3333

3434
type IterableEntry[T] = MapEntry[Int, T]

core/src/main/scala/com/softwaremill/diffx/generic/DiffMagnoliaDerivation.scala

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,12 @@ trait DiffMagnoliaDerivation extends LowPriority {
2121
val map = ListMap(ctx.parameters.map { p =>
2222
val lType = p.dereference(left)
2323
val pType = p.dereference(right)
24-
val fieldDiff = context.getOverride(p.label).map(_.asInstanceOf[Diff[p.PType]]).getOrElse(p.typeclass)
24+
val fieldDiffMod =
25+
context
26+
.getOverride(p.label)
27+
.map(_.asInstanceOf[Diff[p.PType] => Diff[p.PType]])
28+
.getOrElse(identity[Diff[p.PType]] _)
29+
val fieldDiff = fieldDiffMod(p.typeclass)
2530
p.label -> fieldDiff(lType, pType, context.getNextStep(p.label))
2631
}: _*)
2732
DiffResultObject(ctx.typeName.short, map)

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,4 +144,44 @@ class DiffModifyIntegrationTest extends AnyFlatSpec with Matchers {
144144
val d = Diff[Organization].modify(_.people).withListMatcher(om)
145145
compare(o1, o2)(d).isIdentical shouldBe true
146146
}
147+
148+
it should "allow overriding how ignored diffs are produced" in {
149+
implicit val conf: DiffConfiguration = DiffConfiguration(makeIgnored =
150+
(original: Diff[Any]) =>
151+
(left: Any, right: Any, context: DiffContext) => {
152+
IdenticalValue(
153+
s"Ignored but was: ${original.apply(left, right, context).show()(ConsoleColorConfig.noColors)}"
154+
)
155+
}
156+
)
157+
implicit val d: Diff[Person] = Derived[Diff[Person]].ignore(_.name)
158+
compare(p1, p2) shouldBe DiffResultObject(
159+
"Person",
160+
Map(
161+
"name" -> IdenticalValue("Ignored but was: p[1 -> 2]"),
162+
"age" -> DiffResultValue(22, 11),
163+
"in" -> IdenticalValue(instant)
164+
)
165+
)
166+
}
167+
168+
it should "allow overriding how ignored diffs are produced - regular instance" in {
169+
implicit val conf: DiffConfiguration = DiffConfiguration(makeIgnored =
170+
(original: Diff[Any]) =>
171+
(left: Any, right: Any, context: DiffContext) => {
172+
IdenticalValue(
173+
s"Ignored but was: ${original.apply(left, right, context).show()(ConsoleColorConfig.noColors)}"
174+
)
175+
}
176+
)
177+
val d: Diff[Person] = Diff[Person].ignore(_.name)
178+
compare(p1, p2)(d) shouldBe DiffResultObject(
179+
"Person",
180+
Map(
181+
"name" -> IdenticalValue("Ignored but was: p[1 -> 2]"),
182+
"age" -> DiffResultValue(22, 11),
183+
"in" -> IdenticalValue(instant)
184+
)
185+
)
186+
}
147187
}

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ import org.scalatest.freespec.AnyFreeSpec
55
import org.scalatest.matchers.should.Matchers
66

77
class DiffResultTest extends AnyFreeSpec with Matchers with DiffxConsoleSupport {
8-
implicit val colorConfig: ConsoleColorConfig =
9-
ConsoleColorConfig(default = identity, arrow = identity, right = identity, left = identity)
8+
implicit val colorConfig: ConsoleColorConfig = ConsoleColorConfig.noColors
109

1110
"diff set output" - {
1211
"it should show a simple difference" in {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ class DiffSemiautoTest extends AnyFreeSpec with Matchers {
5151
}
5252

5353
"should allow modifying derived diffs" in {
54-
implicit val dACoproduct: Derived[Diff[ProductA]] = Diff.derived[ProductA].modify(_.id).ignore()
54+
implicit val dACoproduct: Derived[Diff[ProductA]] = Diff.derived[ProductA].modify(_.id).ignore
5555

5656
Diff.compare[ProductA](ProductA("1"), ProductA("2")).isIdentical shouldBe true
5757
}

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

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import java.util.UUID
1111
import scala.collection.immutable.ListMap
1212

1313
class DiffTest extends AnyFreeSpec with Matchers {
14-
private val instant: Instant = Instant.now()
14+
val ignored = DiffConfiguration.Default.makeIgnored
15+
val instant: Instant = Instant.now()
1516
val p1 = Person("p1", 22, instant)
1617
val p2 = Person("p2", 11, instant)
1718

@@ -83,7 +84,7 @@ class DiffTest extends AnyFreeSpec with Matchers {
8384
}
8485

8586
"ignored fields should be different than identical" in {
86-
implicit val d: Diff[Person] = Derived[Diff[Person]].modifyUnsafe("name")(Diff.ignored)
87+
implicit val d: Diff[Person] = Derived[Diff[Person]].modifyUnsafe("name")(ignored)
8788
compare(p1, p1.copy(name = "other")) shouldBe DiffResultObject(
8889
"Person",
8990
Map(
@@ -96,7 +97,9 @@ class DiffTest extends AnyFreeSpec with Matchers {
9697

9798
"ignoring given fields" in {
9899
implicit val d: Diff[Person] =
99-
Derived[Diff[Person]].modifyUnsafe("name")(Diff.ignored).modifyUnsafe("age")(Diff.ignored)
100+
Derived[Diff[Person]]
101+
.modifyUnsafe("name")(ignored)
102+
.modifyUnsafe("age")(ignored)
100103
val p3 = p2.copy(in = Instant.now())
101104
compare(p1, p3) shouldBe DiffResultObject(
102105
"Person",
@@ -141,7 +144,7 @@ class DiffTest extends AnyFreeSpec with Matchers {
141144
"nested products ignoring nested fields" in {
142145
val f1 = Family(p1, p2)
143146
val f2 = Family(p1, p1)
144-
implicit val d: Diff[Family] = Derived[Diff[Family]].modifyUnsafe("second", "name")(Diff.ignored)
147+
implicit val d: Diff[Family] = Derived[Diff[Family]].modifyUnsafe("second", "name")(ignored)
145148
compare(f1, f2) shouldBe DiffResultObject(
146149
"Family",
147150
Map(
@@ -169,7 +172,7 @@ class DiffTest extends AnyFreeSpec with Matchers {
169172
val p1p = p1.copy(name = "other")
170173
val f1 = Family(p1, p2)
171174
val f2 = Family(p1p, p2.copy(name = "other"))
172-
implicit val d: Diff[Family] = Derived[Diff[Family]].modifyUnsafe("second", "name")(Diff.ignored)
175+
implicit val d: Diff[Family] = Derived[Diff[Family]].modifyUnsafe("second", "name")(ignored)
173176
compare(f1, f2) shouldBe DiffResultObject(
174177
"Family",
175178
Map(
@@ -196,7 +199,7 @@ class DiffTest extends AnyFreeSpec with Matchers {
196199
"nested products ignoring nested products" in {
197200
val f1 = Family(p1, p2)
198201
val f2 = Family(p1, p1)
199-
implicit val d: Diff[Family] = Derived[Diff[Family]].modifyUnsafe("second")(Diff.ignored)
202+
implicit val d: Diff[Family] = Derived[Diff[Family]].modifyUnsafe("second")(ignored)
200203
compare(f1, f2).isIdentical shouldBe true
201204
}
202205

@@ -357,7 +360,7 @@ class DiffTest extends AnyFreeSpec with Matchers {
357360
"use ignored fields from elements" in {
358361
val o1 = Organization(List(p1, p2))
359362
val o2 = Organization(List(p1, p1, p1))
360-
implicit val d: Diff[Organization] = Derived[Diff[Organization]].modifyUnsafe("people", "name")(Diff.ignored)
363+
implicit val d: Diff[Organization] = Derived[Diff[Organization]].modifyUnsafe("people", "name")(ignored)
361364
compare(o1, o2) shouldBe DiffResultObject(
362365
"Organization",
363366
Map(
@@ -488,7 +491,7 @@ class DiffTest extends AnyFreeSpec with Matchers {
488491
}
489492
"ignored fields from elements" in {
490493
val p2m = p2.copy(age = 33, in = Instant.now())
491-
implicit val d: Diff[Person] = Derived[Diff[Person]].modifyUnsafe("age")(Diff.ignored)
494+
implicit val d: Diff[Person] = Derived[Diff[Person]].modifyUnsafe("age")(ignored)
492495
implicit val im: ObjectMatcher[Person] = ObjectMatcher.by(_.name)
493496
compare(Set(p1, p2), Set(p1, p2m)) shouldBe DiffResultSet(
494497
List(
@@ -527,15 +530,15 @@ class DiffTest extends AnyFreeSpec with Matchers {
527530
"identical when products are identical using ignored" in {
528531
val p2m = p2.copy(age = 33, in = Instant.now())
529532
implicit val d: Diff[Person] = Derived[Diff[Person]]
530-
.modifyUnsafe("age")(Diff.ignored)
531-
.modifyUnsafe("in")(Diff.ignored)
533+
.modifyUnsafe("age")(ignored)
534+
.modifyUnsafe("in")(ignored)
532535
compare(Set(p1, p2), Set(p1, p2m)).isIdentical shouldBe true
533536
}
534537

535538
"propagate ignore fields to elements" in {
536539
val p2m = p2.copy(in = Instant.now())
537540
implicit val im: ObjectMatcher[Person] = ObjectMatcher.by(_.name)
538-
implicit val ds: Diff[Person] = Derived[Diff[Person]].modifyUnsafe("age")(Diff.ignored)
541+
implicit val ds: Diff[Person] = Derived[Diff[Person]].modifyUnsafe("age")(ignored)
539542
compare(Set(p1, p2), Set(p1, p2m)) shouldBe DiffResultSet(
540543
List(
541544
DiffResultObject(
@@ -634,7 +637,7 @@ class DiffTest extends AnyFreeSpec with Matchers {
634637
}
635638

636639
"propagate ignored fields to elements" in {
637-
implicit val dm: Diff[Person] = Derived[Diff[Person]].modifyUnsafe("age")(Diff.ignored)
640+
implicit val dm: Diff[Person] = Derived[Diff[Person]].modifyUnsafe("age")(ignored)
638641
compare(Map("first" -> p1), Map("first" -> p2)) shouldBe DiffResultMap(
639642
Map(
640643
IdenticalValue("first") -> DiffResultObject(
@@ -658,8 +661,8 @@ class DiffTest extends AnyFreeSpec with Matchers {
658661
"identical when products are identical using ignore" in {
659662
implicit val dm: Diff[Person] =
660663
Derived[Diff[Person]]
661-
.modifyUnsafe("age")(Diff.ignored)
662-
.modifyUnsafe("name")(Diff.ignored)
664+
.modifyUnsafe("age")(ignored)
665+
.modifyUnsafe("name")(ignored)
663666
compare(Map("first" -> p1), Map("first" -> p2)).isIdentical shouldBe true
664667
}
665668

0 commit comments

Comments
 (0)