├── .gitignore
├── LICENSE.txt
├── README.md
├── TODO.txt
├── build.sbt
├── example-output.png
├── project
├── build.properties
└── plugins.sbt
└── src
├── main
└── scala
│ └── diff.scala
└── test
└── scala
└── Main.scala
/.gitignore:
--------------------------------------------------------------------------------
1 | target
2 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright 2016 x.ai Inc.
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## diff
2 |
3 | [](https://gitter.im/xdotai/diff?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
4 |
5 | A tool to visually compare Scala data structures with out of the box support for arbitrary case classes.
6 |
7 | Be aware: Collections (List, Seq, etc.) are compared like sets, i.e. ignoring order.
8 |
9 | ### SBT Dependencies
10 |
11 | #### Scala 2.11/2.12
12 |
13 | ```scala
14 | "ai.x" %% "diff" % "2.0.1"
15 | ```
16 |
17 | #### Scala 2.10
18 |
19 | Stopped working in 1.2.0 due to what seems like a Scala compiler bug.
20 | See https://github.com/xdotai/diff/issues/18
21 |
22 |
28 |
29 | ### Usage
30 |
31 | ```scala
32 | import ai.x.diff.DiffShow
33 | import ai.x.diff.conversions._
34 | println( DiffShow.diff[Foo]( before, after ).string )
35 | ```
36 |
37 | Be aware that diff throws an Exception if a DiffShow type class instance for some field
38 | can't be found rather than a type error.
39 | If you use diff in a testing or debugging scenario that's usually not a problem.
40 | The advantage is that the Exception can tell exactly which instance wasn't found. A type error
41 | can only point to the outer most class (`Foo` in this case) even if it is actually one of it's deeply nested fields that is lacking an instance for it's type. Knowing only `Foo` would not be very helpful to pin point
42 | which instance is missing.
43 |
44 | #### Output
45 |
46 |
47 |
48 | #### Code
49 |
50 | ```scala
51 | sealed trait Parent
52 | case class Bar( s: String, i: Int ) extends Parent
53 | case class Foo( bar: Bar, b: List[Int], parent: Option[Parent] ) extends Parent
54 |
55 | val before: Foo = Foo(
56 | Bar( "asdf", 5 ),
57 | List( 123, 1234 ),
58 | Some( Bar( "asdf", 5 ) )
59 | )
60 | val after: Foo = Foo(
61 | Bar( "asdf", 66 ),
62 | List( 1234 ),
63 | Some( Bar( "qwer", 5 ) )
64 | )
65 | ```
66 |
67 | ### Helpful tips
68 |
69 | ```scala
70 | import ai.x.diff._
71 | ```
72 |
73 | #### Custom comparison
74 |
75 | Sometimes you may need to write your own type class instances. For example for non-case classes that don't compare well using ==.
76 |
77 | ```scala
78 | import ai.x.diff._
79 | import org.joda.time.LocalTime
80 |
81 | implicit def localTimeDiffShow: DiffShow[LocalTime] = new DiffShow[LocalTime] {
82 | def show ( d: LocalTime ) = "LocalTime(" + d.toString + ")"
83 | def diff( l: LocalTime, r: LocalTime ) =
84 | if ( l isEqual r ) Identical( l ) else Different( l, r )
85 | }
86 | ```
87 |
88 | #### Ignore parts of data
89 |
90 | Sometimes you may want to ignore some parts of your data during comparison.
91 | You can do so by type, e.g. for non-deterministic parts like ObjectId, which always differ.
92 |
93 | ```scala
94 | def ignore[T]: DiffShow[T] = new DiffShow[T] {
95 | def show( t: T ) = t.toString
96 | def diff( left: T, right: T ) = Identical( "" )
97 | override def diffable( left: T, right: T ) = true
98 | }
99 | implicit def LocationIdShow: DiffShow[LocationId] = ignore[LocationId]
100 | ```
101 |
102 | #### Influence comparison in collections
103 |
104 | When comparing collections you can influence if two elements should be compared or treated as completely different.
105 | Comparing elements shows their partial differences. Not comparing them shows them as added or removed.
106 |
107 | ```scala
108 | implicit def PersonDiffShow[L <: HList](
109 | implicit
110 | labelled: LabelledGeneric.Aux[Person, L],
111 | hlistShow: Lazy[DiffShowFields[L]]
112 | ): DiffShow[Person] = new CaseClassDiffShow[Person, L] {
113 | override def diffable( left: Person, right: Person ) = left._id === right._id
114 | }
115 | ```
116 |
--------------------------------------------------------------------------------
/TODO.txt:
--------------------------------------------------------------------------------
1 | Loose TODO:
2 |
3 | - allow to always show certain fields for selected types even if unchanged in order to see relevant context
4 | - replace many of the manual type classes with shapeless automatic type class derivation https://github.com/milessabin/shapeless/blob/master/core/src/main/scala/shapeless/typeclass.scala
5 | - introduce intermediate representation to allow alternative String renderings and allow easy testability of added/removed
6 | - split Show and Diff
7 |
--------------------------------------------------------------------------------
/build.sbt:
--------------------------------------------------------------------------------
1 | import sbt._
2 | import Keys._
3 | import scalariform.formatter.preferences._
4 | import com.typesafe.sbt.SbtScalariform.{ScalariformKeys, autoImport}
5 | import xerial.sbt.Sonatype._
6 |
7 | val projectName = "diff"
8 |
9 | version := "2.0.1"
10 | name := projectName
11 | scalaVersion := "2.12.8"
12 | crossScalaVersions := Seq("2.11.11", scalaVersion.value)
13 | description := "diff tool for Scala data structures (nested case classes etc)"
14 | libraryDependencies ++= Seq(
15 | "com.chuusai" %% "shapeless" % "2.3.2",
16 | "org.cvogt" %% "scala-extensions" % "0.5.3",
17 | "org.scalactic" %% "scalactic" % "3.0.5",
18 | "org.scalatest" %% "scalatest" % "3.0.5" % "test"
19 | )
20 |
21 | resolvers ++= Seq(Resolver.sonatypeRepo("releases"),Resolver.sonatypeRepo("snapshots"))
22 | useGpg := true
23 |
24 | credentials += Credentials(Path.userHome / ".sbt" / "sonatype_credential")
25 |
26 | testOptions in Test += Tests.Argument(TestFrameworks.ScalaTest, "-oSD")
27 | organizationName := "x.ai"
28 | organization := "ai.x"
29 | scalacOptions in (Compile, doc) ++= Seq(
30 | "-doc-footer", projectName+" is developed by Miguel Iglesias.",
31 | "-implicits",
32 | "-groups"
33 | )
34 | scalariformPreferences := scalariformPreferences.value
35 | .setPreference(AlignParameters, true)
36 | .setPreference(AlignArguments, true)
37 | .setPreference(AlignSingleLineCaseStatements, true)
38 | .setPreference(MultilineScaladocCommentsStartOnFirstLine, true)
39 | .setPreference(SpaceInsideParentheses, true)
40 | .setPreference(SpacesWithinPatternBinders, true)
41 | .setPreference(SpacesAroundMultiImports, true)
42 | .setPreference(DanglingCloseParenthesis, Preserve)
43 | .setPreference(DoubleIndentConstructorArguments, true)
44 | publishTo := sonatypePublishTo.value
45 | publishMavenStyle := true
46 | publishArtifact in Test := false
47 | pomIncludeRepository := { _ => false }
48 | licenses += ("Apache 2.0", url("http://github.com/xdotai/"+projectName+"/blob/master/LICENSE.txt"))
49 | homepage := Some(url("http://github.com/xdotai/"+projectName))
50 | startYear := Some(2016)
51 | scmInfo := Some(
52 | ScmInfo(
53 | url(s"https://github.com/xdotai/$projectName"),
54 | s"scm:git@github.com:xdotai/$projectName.git"
55 | )
56 | )
57 | developers := List(
58 | Developer(id="caente", name="Miguel Iglesias", email="miguel.iglesias@human.x.ai", url=url("https://github.com/caente"))
59 | )
60 |
--------------------------------------------------------------------------------
/example-output.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bizzabo/diff/8b0a9e24fd6edd42607da812ac84374db71597df/example-output.png
--------------------------------------------------------------------------------
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=1.1.4
2 |
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | addSbtPlugin("org.scalariform" % "sbt-scalariform" % "1.8.2")
2 | addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.0.3")
3 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "2.3")
4 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.0")
5 |
6 |
--------------------------------------------------------------------------------
/src/main/scala/diff.scala:
--------------------------------------------------------------------------------
1 | package ai.x.diff
2 | import shapeless._, record._, shapeless.syntax._, labelled._, ops.record._, ops.hlist._
3 | import org.cvogt.scala.StringExtensions
4 | import java.util.UUID
5 | import org.cvogt.scala.debug.ThrowableExtensions
6 | import org.cvogt.scala.constraint.{ CaseClass, SingletonObject, boolean }
7 |
8 | object `package` {
9 | def red( s: String ) = Console.RED + s + Console.RESET
10 | def green( s: String ) = Console.GREEN + s + Console.RESET
11 | def blue( s: String ) = Console.BLUE + s + Console.RESET
12 | def pad( s: Any, i: Int = 5 ) = ( " " * ( i - s.toString.size ) ) + s
13 | def arrow( l: String, r: String ) = l + " -> " + r
14 | def showChangeRaw( l: String, r: String ): String = red( l ) + " -> " + green( r )
15 | def showChange[L: DiffShow, R: DiffShow]( l: L, r: R ): String = showChangeRaw( DiffShow.show( l ), DiffShow.show( r ) )
16 |
17 | /** Hack to avoid scala compiler bug with getSimpleName
18 | * @see https://issues.scala-lang.org/browse/SI-2034
19 | */
20 | private[diff] def getClassSimpleName( klass: Class[_] ): String = {
21 | try {
22 | klass.getSimpleName
23 | } catch {
24 | case _: InternalError ⇒
25 | val fullName = klass.getName.stripSuffix( "$" )
26 | val fullClassName = fullName.substring( fullName.lastIndexOf( "." ) + 1 )
27 | fullClassName.substring( fullClassName.lastIndexOf( "$" ) + 1 )
28 | }
29 | }
30 | }
31 |
32 | object conversions {
33 | implicit def seqAsSet[E: DiffShow]: DiffShow[Seq[E]] = new DiffShow[Seq[E]] {
34 | def show( t: Seq[E] ) = DiffShow[Set[E]].show( t.toSet )
35 | def diff( left: Seq[E], right: Seq[E] ) = DiffShow[Set[E]].diff( left.toSet, right.toSet )
36 | }
37 | }
38 |
39 | abstract class Comparison {
40 | def string: String
41 | def create( s: String ): Comparison
42 | def map( f: String => String ): Comparison = create( f( this.string ) )
43 | def flatMap( f: String => Comparison ): Comparison = f( this.string ) match {
44 | case Identical( s ) => create( s )
45 | case c: Different => c
46 | case c: Error => c
47 | }
48 | def isIdentical: Boolean
49 | }
50 | case class Identical private ( string: String ) extends Comparison {
51 | def create( s: String ) = Identical( s )
52 | override def isIdentical = true
53 | }
54 | object Identical {
55 | def apply[T: DiffShow]( value: T ): Identical = Identical( DiffShow.show( value ) )
56 | }
57 | case class Different private ( string: String ) extends Comparison {
58 | def create( s: String ) = Different( s )
59 | override def isIdentical = false
60 | }
61 | case class Error( string: String ) extends Comparison {
62 | def create( s: String ) = Error( s )
63 | override def isIdentical = throw new Exception( string )
64 | }
65 | object Different {
66 | def apply[L: DiffShow, R: DiffShow]( left: L, right: R ): Different = Different( showChange( left, right ) )
67 | }
68 | abstract class DiffShow[-T] {
69 | def show( t: T ): String
70 | def diff( left: T, right: T ): Comparison
71 | def diffable( left: T, right: T ) = diff( left, right ).isIdentical
72 | }
73 |
74 | object DiffShow extends DiffShowInstances {
75 | def apply[T]( implicit diffShow: DiffShow[T] ) = diffShow
76 | def show[T]( t: T )( implicit diffShow: DiffShow[T] ): String = diffShow.show( t )
77 | def diff[T]( left: T, right: T )( implicit diffShow: DiffShow[T] ): Comparison = diffShow.diff( left, right )
78 | def diffable[T]( left: T, right: T )( implicit diffShow: DiffShow[T] ) = diffShow.diffable( left, right )
79 | }
80 | abstract class DiffShowFields[-T] { // contra-variant to allow Seq type class for List
81 | def show( t: T ): Map[String, String]
82 | def diff( left: T, right: T ): Map[String, Comparison]
83 | }
84 |
85 | abstract class DiffShowFieldsLowPriority {
86 | implicit def otherDiffShowFields[T: scala.reflect.ClassTag]: DiffShowFields[T] = new DiffShowFields[T] {
87 | val T = scala.reflect.classTag[T].toString
88 | // throw new Exception( s"Cannot find DiffShowFields[$T]" )
89 | def show( v: T ) = throw new Exception( s"Cannot find DiffShowFields[$T] to show value " + v )
90 | def diff( l: T, r: T ) = throw new Exception( s"Cannot find DiffShowFields[$T] to diff values ($l, $r)" )
91 | }
92 | }
93 |
94 | object DiffShowFields {
95 | def apply[T]( implicit show: DiffShowFields[T] ): DiffShowFields[T] = show
96 |
97 | implicit object hNil extends DiffShowFields[HNil] {
98 | def show( t: HNil ) = Map()
99 | def diff( left: HNil, right: HNil ) = Map()
100 | }
101 |
102 | implicit def hCons[Key <: Symbol, Value, Tail <: HList](
103 | implicit
104 | key: Witness.Aux[Key],
105 | showHead: DiffShow[Value],
106 | showTail: Lazy[DiffShowFields[Tail]]
107 | ): DiffShowFields[FieldType[Key, Value] :: Tail] =
108 | new DiffShowFields[FieldType[Key, Value] :: Tail] {
109 | def show( hlist: FieldType[Key, Value] :: Tail ) =
110 | showTail.value.show( hlist.tail ) + ( key.value.name -> showHead.show( hlist.head ) )
111 | def diff( left: FieldType[Key, Value] :: Tail, right: FieldType[Key, Value] :: Tail ) = {
112 | showTail.value.diff( left.tail, right.tail ) + ( key.value.name -> DiffShow.diff[Value]( left.head, right.head ) )
113 | }
114 | }
115 | }
116 |
117 | abstract class DiffShowInstancesLowPriority {
118 | // enable for debugging if your type class can't be found
119 | implicit def otherDiffShow[T: scala.reflect.ClassTag]: DiffShow[T] = new DiffShow[T] {
120 | private val T = scala.reflect.classTag[T].toString
121 | // throw new Exception( s"Cannot find DiffShow[$T]" )
122 | def show( v: T ) = red( new Exception( s"ERROR: Cannot find DiffShow[$T] to show value " + v ).showStackTrace )
123 | def diff( l: T, r: T ) = Error( new Exception( s"ERROR: Cannot find DiffShow[$T] to show values ($l, $r)" ).showStackTrace )
124 | }
125 | }
126 |
127 | abstract class DiffShowInstances extends DiffShowInstances2 {
128 | implicit def optionDiffShow[T: DiffShow]: DiffShow[Option[T]] = new DiffShow[Option[T]] {
129 | def show( t: Option[T] ) = t match {
130 | case None => "None"
131 | case Some( v ) => constructor( "Some", List( "" -> DiffShow.show( v ) ) )
132 | }
133 | def diff( l: Option[T], r: Option[T] ) = ( l, r ) match {
134 | case ( None, None ) => Identical( None )
135 | case ( Some( _l ), Some( _r ) ) => DiffShow.diff( _l, _r ).map( s => constructor( "Some", List( "" -> s ) ) )
136 | case ( _l, _r ) => Different( _l, _r )
137 | }
138 | }
139 | implicit def someDiffShow[T]( implicit ds: DiffShow[T] ) = new DiffShow[Some[T]] {
140 | def show( s: Some[T] ) = constructor( "Some", List( "" -> ds.show( s.get ) ) )
141 | def diff( l: Some[T], r: Some[T] ) = ds.diff( l.get, r.get ).map( s => constructor( "Some", List( "" -> s ) ) )
142 | }
143 |
144 | implicit def singletonObjectDiffShow[T: SingletonObject]: DiffShow[T] = new DiffShow[T] {
145 | def show( t: T ) = getClassSimpleName( t.getClass )
146 | def diff( l: T, r: T ) = Identical( l )
147 | }
148 |
149 | def primitive[T]( show: T => String ) = {
150 | val _show = show
151 | new DiffShow[T] {
152 | implicit val diffShow = this
153 | def show( t: T ) = _show( t )
154 | def diff( left: T, right: T ) = if ( left == right ) Identical( left ) else Different( left, right )
155 | }
156 | }
157 |
158 | // instances for primitive types
159 |
160 | implicit def booleanDiffShow: DiffShow[Boolean] = primitive( _.toString )
161 | implicit def floatDiffShow: DiffShow[Float] = primitive( _.toString )
162 | implicit def doubleDiffShow: DiffShow[Double] = primitive( _.toString )
163 | implicit def intDiffShow: DiffShow[Int] = primitive( _.toString )
164 | implicit def longDiffShow: DiffShow[Long] = primitive( _.toString )
165 | implicit def stringDiffShow: DiffShow[String] = primitive( s => "\"" ++ s ++ "\"" )
166 |
167 | // instances for some common types
168 |
169 | implicit val uuidDiffShow: DiffShow[UUID] = primitive[UUID]( _.toString )
170 |
171 | implicit def eitherDiffShow[L: DiffShow, R: DiffShow]: DiffShow[Either[L, R]] = {
172 | new DiffShow[Either[L, R]] {
173 | def show( e: Either[L, R] ) = constructor(
174 | getClassSimpleName( e.getClass ),
175 | List( "" -> ( e.fold( DiffShow.show( _ ), DiffShow.show( _ ) ) ) )
176 | )
177 | def diff( l: Either[L, R], r: Either[L, R] ) = {
178 | if ( l == r ) {
179 | Identical( show( l ) )
180 | } else {
181 | Different( ( l, r ) match {
182 | case ( Left( firstValue ), Left( secondValue ) ) =>
183 | constructor( "Left", List( "" -> showChange( firstValue, secondValue ) ) )
184 | case ( Right( firstValue ), Right( secondValue ) ) =>
185 | constructor( "Right", List( "" -> showChange( firstValue, secondValue ) ) )
186 | case ( firstValue, secondValue ) =>
187 | showChange( firstValue, secondValue )
188 | } )
189 | }
190 | }
191 | }
192 | }
193 |
194 | implicit def caseClassDiffShow[T <: Product with Serializable: CaseClass, L <: HList](
195 | implicit
196 | ev: boolean.![SingletonObject[T]],
197 | labelled: LabelledGeneric.Aux[T, L],
198 | hlistShow: Lazy[DiffShowFields[L]]
199 | ): DiffShow[T] = new CaseClassDiffShow[T, L]
200 |
201 | /** reusable class for case class instances, can be used to customize "diffable" for specific case classes */
202 | class CaseClassDiffShow[T: CaseClass, L <: HList](
203 | implicit
204 | labelled: LabelledGeneric.Aux[T, L],
205 | hlistShow: Lazy[DiffShowFields[L]]
206 | ) extends DiffShow[T] {
207 | implicit val diffShow = this
208 |
209 | def show( t: T ) = {
210 | val fields = hlistShow.value.show( labelled to t ).toList.sortBy( _._1 )
211 | constructor( getClassSimpleName( t.getClass ), fields )
212 | }
213 | def diff( left: T, right: T ) = {
214 | val fields = hlistShow.value.diff( labelled to left, labelled to right ).toList.sortBy( _._1 ).map {
215 | case ( name, Different( value ) ) => Some( name -> value )
216 | case ( name, Error( value ) ) => Some( name -> value )
217 | case ( name, Identical( _ ) ) => None
218 | }
219 | if ( fields.flatten.nonEmpty ) Different(
220 | constructorOption( getClassSimpleName( left.getClass ), fields )
221 | )
222 | else Identical( left )
223 | }
224 | }
225 | }
226 | abstract class DiffShowInstances2 extends DiffShowInstancesLowPriority {
227 | self: DiffShowInstances =>
228 |
229 | // helper methods
230 | def constructor( name: String, keyValues: List[( String, String )] ): String = constructorOption( name, keyValues.map( Option( _ ) ) )
231 | def constructorOption( name: String, keyValues: List[Option[( String, String )]] ): String = {
232 | val suppressed = keyValues.contains( None )
233 | val args = keyValues.collect {
234 | case Some( ( "", r ) ) => r
235 | case Some( ( l, r ) ) => s"$l = $r"
236 | }
237 |
238 | val inlined = args.mkString( ", " )
239 | name.stripSuffix( "$" ) + (
240 | if ( keyValues.isEmpty ) (
241 | if ( name.endsWith( "$" ) ) "" // case object
242 | else if ( suppressed ) "(...)" // unchanged collection
243 | else "()" // EmptyCaseClass() or List()
244 | )
245 | else if ( keyValues.size == 1 && keyValues.flatten.size == 1 ) (
246 | if ( inlined.contains( "\n" ) ) s"(\n${keyValues.flatten.head._2.indent( 1 )}\n)" // avoid x in Some( x = ... )
247 | else s"( ${keyValues.flatten.head._2} )" // avoid x in Some(\n x = ... \n)
248 | )
249 | else "(" ++ ( if ( suppressed ) " ...," else "" ) ++ (
250 | if ( inlined.size < 120 ) s""" ${inlined} """ // short enough content to inline
251 | else ( "\n" + args.mkString( ",\n" ).indent( 1 ) ++ "\n" ) // long content, break lines
252 | ) ++ ")"
253 | )
254 | }
255 |
256 | // instances for Scala collection types
257 |
258 | // TODO: this should probably Set[T] and Seq[T] in our case be a converter instance on top of it
259 | implicit def setDiffShow[T: DiffShow]: DiffShow[Set[T]] = new DiffShow[Set[T]] {
260 | // this is hacky and requires an asInstanceOf. Mayber there is a cleaner implementation.
261 | override def show( l: Set[T] ): String = {
262 | val fields = l.map( v => DiffShow.show( v ) ).toList
263 | constructor( "Set", fields.map( ( "", _ ) ) )
264 | }
265 |
266 | override def diff( left: Set[T], right: Set[T] ): ai.x.diff.Comparison = {
267 | var _right = right diff left
268 | val results = ( left diff right ).map { l =>
269 | _right.find( DiffShow.diffable( l, _ ) ).map {
270 | r =>
271 | _right = _right - r
272 | Right(
273 | DiffShow.diff( l, r )
274 | )
275 | }.getOrElse( Left( l ) )
276 | }
277 | val removed = results.flatMap( _.left.toOption.map( r => Some( "" -> red( DiffShow.show[T]( r ) ) ) ) ).toList
278 | val added = _right.map( r => Some( "" -> green( DiffShow.show[T]( r ) ) ) ).toList
279 | val ( identical, changed ) = results.flatMap( _.right.toOption.map {
280 | case Identical( s ) => None
281 | case Different( s ) => Some( "" -> s )
282 | } ).toList.partition( _.isEmpty )
283 |
284 | if ( removed.isEmpty && added.isEmpty && changed.isEmpty )
285 | Identical( show( left ) )
286 | else
287 | Different(
288 | constructorOption(
289 | "Set",
290 | changed ++ removed ++ added
291 | ++ ( if ( ( identical ++ ( left intersect right ) ).nonEmpty ) Some( None ) else None )
292 | )
293 | )
294 | }
295 | }
296 |
297 | implicit def mapDiffShow[K: DiffShow, V: DiffShow]: DiffShow[Map[K, V]] = new DiffShow[Map[K, V]] {
298 | def show( l: Map[K, V] ) = (
299 | constructor(
300 | "Map",
301 | l.map { case ( k, v ) => DiffShow.show( k ) -> DiffShow.show( v ) }.map( ( arrow _ ).tupled ).map( ( "", _ ) ).toList
302 | )
303 | )
304 | def diff( left: Map[K, V], right: Map[K, V] ) = {
305 | val identical = left.keys.toList intersect right.keys.toList
306 | val removed = left.keys.toList diff right.keys.toList
307 | val added = right.keys.toList diff left.keys.toList
308 | def show( keys: List[K] ) = keys.map( k => DiffShow.show( k ) -> DiffShow.show( right( k ) ) )
309 | val changed = for {
310 | key <- left.keys.toList diff removed
311 | Different( s ) <- DiffShow.diff( left( key ), right( key ) ) :: Nil
312 | } yield s
313 |
314 | val string =
315 | constructorOption(
316 | "Map",
317 | identical.map( _ => None ) ++ Seq(
318 | changed,
319 | show( removed ).map( ( arrow _ ).tupled ).map( red ),
320 | show( added ).map( ( arrow _ ).tupled ).map( green )
321 | ).flatten.map( s => Option( ( "", s ) ) )
322 | )
323 | if ( removed.isEmpty && added.isEmpty && changed.isEmpty )
324 | Identical( string )
325 | else
326 | Different( string )
327 | }
328 | }
329 |
330 | // instances for Shapeless types
331 |
332 | implicit object CNilDiffShow extends DiffShow[CNil] {
333 | def show( t: CNil ) = throw new Exception( "Methods in CNil type class instance should never be called. Right shapeless?" )
334 | def diff( left: CNil, right: CNil ) = throw new Exception( "Methods in CNil type class instance should never be called. Right shapeless?" )
335 | }
336 |
337 | implicit def coproductDiffShow[Name <: Symbol, Head, Tail <: Coproduct](
338 | implicit
339 | headShow: DiffShow[Head],
340 | tailShow: DiffShow[Tail]
341 | ): DiffShow[Head :+: Tail] = new DiffShow[Head :+: Tail] {
342 | def show( co: Head :+: Tail ) = {
343 | co match {
344 | case Inl( found ) => headShow.show( found )
345 | case Inr( tail ) => tailShow.show( tail )
346 | }
347 | }
348 | def diff( left: Head :+: Tail, right: Head :+: Tail ) = {
349 | ( left, right ) match {
350 | case ( Inl( l ), Inl( r ) ) => headShow.diff( l, r )
351 | case ( Inr( l ), Inr( r ) ) => tailShow.diff( l, r )
352 | case ( Inl( l ), Inr( r ) ) => Different( l, r )
353 | case ( Inr( l ), Inl( r ) ) => Different( l, r )
354 | }
355 | }
356 | }
357 |
358 | implicit def sealedDiffShow[T, L <: Coproduct](
359 | implicit
360 | coproduct: Generic.Aux[T, L],
361 | coproductShow: Lazy[DiffShow[L]]
362 | ): DiffShow[T] = new DiffShow[T] {
363 | def show( t: T ) = coproductShow.value.show( coproduct.to( t ) )
364 | def diff( l: T, r: T ) = coproductShow.value.diff( coproduct.to( l ), coproduct.to( r ) )
365 | }
366 | implicit val diffShow = this
367 | }
368 |
--------------------------------------------------------------------------------
/src/test/scala/Main.scala:
--------------------------------------------------------------------------------
1 | import ai.x.diff._
2 | import ai.x.diff.conversions._
3 | import scala.collection.immutable.SortedMap
4 | import java.util.UUID
5 | import org.scalatest.FunSuite
6 |
7 | object Foo1 {
8 | object Foo2 {
9 | case object Foo3
10 | }
11 | }
12 |
13 | sealed trait Parent
14 | case class Bar( s: String, i: Int ) extends Parent
15 | case class Foo( bar: Bar, b: List[Int], parent: Option[Parent] ) extends Parent
16 |
17 | case class Id( int: Int )
18 | case class Row( id: Id, value: String )
19 | class AllTests extends FunSuite {
20 | test( "all tests" ) {
21 | val bar = Bar( "test", 1 )
22 | val barAsParent: Parent = bar
23 | val barString = """Bar( i = 1, s = "test" )"""
24 | val foo = Foo( bar, Nil, None )
25 | val fooAsParent: Parent = foo
26 | val fooString = """Foo( b = Set(), bar = Bar( i = 1, s = "test" ), parent = None )"""
27 |
28 | def assertIdentical[T: DiffShow]( left: T, right: T, expectedOutput: String = null ) = {
29 | val c = DiffShow.diff( left, right )
30 | assert( c.isIdentical, c.toString )
31 | Option( expectedOutput ).foreach(
32 | e => assert( c.string == e, "Expected: " ++ e ++ " Found: " ++ c.string )
33 | )
34 | assert( c.string == DiffShow.show( left ), "Expected: " ++ DiffShow.show( left ) ++ " Found: " ++ c.string )
35 | }
36 | def assertNotIdentical[T: DiffShow]( left: T, right: T, expectedOutput: String = null ) = {
37 | val c = DiffShow.diff( left, right )
38 | assert( !c.isIdentical, c.toString )
39 | Option( expectedOutput ).foreach {
40 | e => assert( c.string == e, "Expected: " ++ e ++ " Found: " ++ c.string )
41 | }
42 | }
43 |
44 | assertIdentical( bar, bar, barString )
45 | assertIdentical( barAsParent, barAsParent, barString )
46 | assertIdentical( bar, barAsParent, barString )
47 | assertIdentical( barAsParent, bar, barString )
48 | assertIdentical( foo, foo, fooString )
49 | assertIdentical( foo, fooAsParent, fooString )
50 | assertIdentical( fooAsParent, foo, fooString )
51 | assertIdentical( fooAsParent, fooAsParent, fooString )
52 |
53 | assertNotIdentical[Parent]( bar, foo, showChange( bar, foo ) )
54 | assertNotIdentical( bar, fooAsParent, showChange( bar, foo ) )
55 | assertNotIdentical( barAsParent, foo, showChange( bar, foo ) )
56 | assertNotIdentical( barAsParent, fooAsParent, showChange( bar, foo ) )
57 |
58 | assertIdentical( Seq( bar ), Seq( bar ) )
59 | // Seqs are compared as Sets
60 | assertIdentical( Seq( bar ), Seq( bar, bar ) )
61 |
62 | assertNotIdentical[Seq[Parent]]( Seq( foo, bar ), Seq( bar ) )
63 | assertNotIdentical[Seq[Parent]]( Seq( foo ), Seq( bar ) )
64 |
65 | {
66 | val uuid1 = UUID.randomUUID()
67 | val uuidAs1 = UUID.fromString( uuid1.toString )
68 | val uuid2 = UUID.randomUUID()
69 | assertIdentical( uuid1, uuidAs1 )
70 | assertNotIdentical( uuid1, uuid2 )
71 | }
72 |
73 | {
74 | val leftEitherFoo = Left( "foo" )
75 | val leftEitherBar = Left( "bar" )
76 | val rightEitherFoo = Right( "foo" )
77 | val rightEitherBar = Right( "bar" )
78 |
79 | val eitherDiff1 = DiffShow.diff( leftEitherFoo, rightEitherFoo )
80 | assert( !eitherDiff1.isIdentical )
81 | assert( eitherDiff1.string == showChangeRaw( """Left( "foo" )""", """Right( "foo" )""" ), eitherDiff1.string )
82 |
83 | val eitherDiff2 = DiffShow.diff( leftEitherFoo, leftEitherBar )
84 | assert( !eitherDiff2.isIdentical )
85 | assert( eitherDiff2.string == s"""Left( ${showChangeRaw( "\"foo\"", "\"bar\"" )} )""", eitherDiff2.string )
86 |
87 | val eitherDiff3 = DiffShow.diff( rightEitherFoo, rightEitherBar )
88 | assert( !eitherDiff3.isIdentical )
89 | assert( eitherDiff3.string == s"""Right( ${showChangeRaw( "\"foo\"", "\"bar\"" )} )""" )
90 |
91 | assert( DiffShow.diff( leftEitherFoo, leftEitherFoo ).isIdentical )
92 | assert( DiffShow.diff( rightEitherFoo, rightEitherFoo ).isIdentical )
93 | }
94 |
95 | def ignore[T] = new DiffShow[T] {
96 | def show( t: T ) = t.toString
97 | def diff( left: T, right: T ) = Identical( "" )
98 | override def diffable( left: T, right: T ) = true
99 | }
100 |
101 | {
102 | implicit val ignoreId = ignore[Id]
103 | assert( DiffShow.diff( Id( 1 ), Id( 1 ) ).isIdentical )
104 | assert( DiffShow.diff( Id( 1 ), Id( 2 ) ).isIdentical )
105 |
106 | val rowA = Row( Id( 1 ), "foo" )
107 | val rowB = Row( Id( 2 ), "foo" )
108 | assert( DiffShow.diff( rowA, rowB ).isIdentical )
109 | assert( DiffShow.diff( Seq( rowA ), Seq( rowB ) ).isIdentical )
110 | }
111 |
112 | assertIdentical( Id( 1 ), Id( 1 ) )
113 | assertNotIdentical( Id( 1 ), Id( 2 ) )
114 |
115 | val rowA = Row( Id( 1 ), "foo" )
116 | val rowB = Row( Id( 2 ), "foo" )
117 | assertNotIdentical( rowA, rowB )
118 | assertNotIdentical( Seq( rowA ), Seq( rowB ) )
119 |
120 | /*
121 | val before: Foo = Foo(
122 | Bar( "asdf", 5 ),
123 | List( 123, 1234 ),
124 | Some( Bar( "asdf", 5 ) )
125 | )
126 | val after: Foo = Foo(
127 | Bar( "asdf", 66 ),
128 | List( 1234 ),
129 | Some( Bar( "qwer", 5 ) )
130 | )
131 |
132 | println(
133 | DiffShow.diff( before, after ).string
134 | )
135 | */
136 |
137 | {
138 | implicit def StringDiffShow: DiffShow[String] = new DiffShow[String] {
139 | def show( t: String ) = "\"" ++ t ++ "\""
140 | def diff( left: String, right: String ) = if ( left == right ) Identical( left ) else Different( left, right )( this, this )
141 | override def diffable( left: String, right: String ) = left.lift( 0 ) == right.lift( 0 )
142 | }
143 |
144 | println(
145 | DiffShow.diff(
146 | "x" :: Nil,
147 | "x" :: Nil
148 | ).string
149 | )
150 | println(
151 | DiffShow.diff(
152 | "x" :: Nil,
153 | Nil
154 | ).string
155 | )
156 | println(
157 | DiffShow.diff(
158 | Nil,
159 | "x" :: Nil
160 | ).string
161 | )
162 | println(
163 | DiffShow.diff(
164 | "adsf" :: "qwer" :: "x" :: Nil,
165 | "axx" :: "yxcv" :: "x" :: Nil
166 | ).string
167 | )
168 | println(
169 | DiffShow.diff(
170 | "adsf" :: "qwer" :: Nil,
171 | "axx" :: "yxcv" :: Nil
172 | ).string
173 | )
174 | }
175 |
176 | {
177 | // testing hack for https://issues.scala-lang.org/browse/SI-2034
178 | assertIdentical( Foo1.Foo2.Foo3, Foo1.Foo2.Foo3 )
179 | }
180 |
181 | /*
182 |
183 | //import pprint.Config.Defaults._
184 |
185 | val actual = compare( before, after )
186 | val expected = Different(
187 | Tree( before ),
188 | Tree( after ),
189 | SortedMap(
190 | "bar" -> Different(
191 | Tree( Bar( "asdf", 5 ) ),
192 | Tree( Bar( "asdf", 66 ) ),
193 | SortedMap(
194 | "s" -> Identical( Leaf( "asdf" ) ),
195 | "i" -> Different(
196 | Leaf( 5 ), Leaf( 66 ), SortedMap()
197 | )
198 | )
199 | ),
200 | "b" -> Different(
201 | Leaf( 123 :: 1234 :: Nil ), Leaf( 1234 :: Nil ), SortedMap()
202 | )
203 | )
204 | )
205 | //pprint.pprintln( ( Generic[Bar] to Bar( "asdf", 5 ) ) delta ( Generic[Bar] to Bar( "asdf", 66 ) ) )
206 | assert( actual == expected, "expected\n:" + pprint.tokenize( expected ).mkString + "\n\nactual:\n" + pprint.tokenize( actual ).mkString )
207 |
208 | println( expected.show() )
209 |
210 | """
211 | Foo(
212 | b = List( 123 +, 1234 ),
213 | bar = Bar(
214 | s = asdf,
215 | i = -5 +66
216 | )
217 | )
218 | """
219 | */
220 | }
221 | }
222 |
--------------------------------------------------------------------------------