├── .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 | [![Join the chat at https://gitter.im/xdotai/diff](https://badges.gitter.im/xdotai/diff.svg)](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 | example-output 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 | --------------------------------------------------------------------------------