diff --git a/project/BuildHelper.scala b/project/BuildHelper.scala index 8f58cfe20..043da04a7 100644 --- a/project/BuildHelper.scala +++ b/project/BuildHelper.scala @@ -9,7 +9,7 @@ import scalafix.sbt.ScalafixPlugin.autoImport._ object BuildHelper { - val zioVersion = "1.0.7" + val zioVersion = "1.0.9" val zioNioVersion = "1.0.0-RC9" val zioJsonVersion = "0.1.2" val silencerVersion = "1.7.1" diff --git a/zio-schema/shared/src/main/scala/zio/schema/Diff.scala b/zio-schema/shared/src/main/scala/zio/schema/Diff.scala new file mode 100644 index 000000000..e28a84dd0 --- /dev/null +++ b/zio-schema/shared/src/main/scala/zio/schema/Diff.scala @@ -0,0 +1,935 @@ +package zio.schema + +import java.math.BigInteger +import java.time.temporal.{ ChronoUnit, Temporal => JTemporal, TemporalAmount, TemporalUnit } +import java.time.{ + DayOfWeek, + Duration => JDuration, + Instant, + LocalDate, + LocalDateTime, + LocalTime, + Month => JMonth, + MonthDay, + OffsetDateTime, + OffsetTime, + Year, + YearMonth, + ZoneId, + ZonedDateTime +} + +import scala.collection.immutable.ListMap + +import zio.Chunk +import zio.schema.internal.MyersDiff + +trait Differ[A] { self => + + def apply(thisValue: A, thatValue: A): Diff + + /** + * A symbolic operator for [[zip]]. + */ + def <*>[B](that: Differ[B]): Differ[(A, B)] = self.zip(that) + + def zip[B](that: Differ[B]): Differ[(A, B)] = Differ.tuple(self, that) + + def transform[B](f: B => A): Differ[B] = (thisValue: B, thatValue: B) => self.apply(f(thisValue), f(thatValue)) + + def transformOrFail[B](f: B => Either[String, A]): Differ[B] = + (thisValue: B, thatValue: B) => + f(thisValue) -> f(thatValue) match { + case (Right(l), Right(r)) => self(l, r) + case _ => Diff.NotComparable + } + + def foreach[Col[_]](toChunk: Col[A] => Chunk[A]): Differ[Col[A]] = + (theseAs: Col[A], thoseAs: Col[A]) => + Diff + .Sequence( + toChunk(theseAs).zipAll(toChunk(thoseAs)).map { + case (Some(left), Some(right)) => self.apply(left, right) + case (None, Some(right)) => Diff.Total(right, Diff.Tag.Right) + case (Some(left), None) => Diff.Total(left, Diff.Tag.Left) + case (None, None) => Diff.Identical + } + ) + .orIdentical + + def optional: Differ[Option[A]] = Differ.instancePartial { + case (Some(l), Some(r)) => self(l, r) + case (Some(l), None) => Diff.Total(l, Diff.Tag.Left) + case (None, Some(r)) => Diff.Total(r, Diff.Tag.Right) + case (None, None) => Diff.Identical + } +} + +object Differ { + + def fromSchema[A](schema: Schema[A]): Differ[A] = schema match { + case Schema.Primitive(StandardType.BinaryType) => binary + case Schema.Primitive(StandardType.IntType) => numeric[Int] + case Schema.Primitive(StandardType.ShortType) => numeric[Short] + case Schema.Primitive(StandardType.DoubleType) => numeric[Double] + case Schema.Primitive(StandardType.FloatType) => numeric[Float] + case Schema.Primitive(StandardType.LongType) => numeric[Long] + case Schema.Primitive(StandardType.CharType) => numeric[Char] + case Schema.Primitive(StandardType.BoolType) => bool + case Schema.Primitive(StandardType.BigDecimalType) => bigDecimal + case Schema.Primitive(StandardType.BigIntegerType) => bigInt + case Schema.Primitive(StandardType.StringType) => string + case Schema.Primitive(StandardType.DayOfWeekType) => dayOfWeek + case Schema.Primitive(StandardType.Month) => month + case Schema.Primitive(StandardType.MonthDay) => monthDay + case Schema.Primitive(StandardType.Year) => + temporal[Year](ChronoUnit.YEARS) + case Schema.Primitive(StandardType.YearMonth) => + temporal[YearMonth](ChronoUnit.MONTHS) + case Schema.Primitive(StandardType.ZoneId) => string.transform[ZoneId](_.getId) + case Schema.Primitive(StandardType.Instant(_)) => + temporal[Instant](ChronoUnit.MILLIS) + case Schema.Primitive(StandardType.Duration(_)) => + temporalAmount[JDuration](ChronoUnit.MILLIS) + case Schema.Primitive(StandardType.LocalDate(_)) => + temporal[LocalDate](ChronoUnit.DAYS) + case Schema.Primitive(StandardType.LocalTime(_)) => + temporal[LocalTime](ChronoUnit.MILLIS) + case Schema.Primitive(StandardType.LocalDateTime(_)) => + temporal[LocalDateTime](ChronoUnit.MILLIS) + case Schema.Primitive(StandardType.OffsetTime(_)) => + temporal[OffsetTime](ChronoUnit.MILLIS) + case Schema.Primitive(StandardType.OffsetDateTime(_)) => + temporal[OffsetDateTime](ChronoUnit.MILLIS) + case Schema.Primitive(StandardType.ZonedDateTime(_)) => + temporal[ZonedDateTime](ChronoUnit.MILLIS) + case Schema.Tuple(leftSchema, rightSchema) => fromSchema(leftSchema) <*> fromSchema(rightSchema) + case Schema.Optional(schema) => fromSchema(schema).optional + case Schema.Sequence(schema, _, f) => fromSchema(schema).foreach(f) + case Schema.EitherSchema(leftSchema, rightSchema) => either(fromSchema(leftSchema), fromSchema(rightSchema)) + case s @ Schema.Lazy(_) => fromSchema(s.schema) + case Schema.Transform(schema, _, f) => fromSchema(schema).transformOrFail(f) + case Schema.Fail(_) => fail + case Schema.GenericRecord(structure) => record(structure) + case ProductDiffer(differ) => differ + case Schema.Enum1(c) => enum(c) + case Schema.Enum2(c1, c2) => enum(c1, c2) + case Schema.Enum3(c1, c2, c3) => enum(c1, c2, c3) + case Schema.EnumN(cs) => enum(cs: _*) + case Schema.Enumeration(structure) => enumeration(structure) + } + + def binary: Differ[Chunk[Byte]] = + (theseBytes: Chunk[Byte], thoseBytes: Chunk[Byte]) => + Diff.Sequence { + theseBytes.zipAll(thoseBytes).map { + case (Some(thisByte), Some(thatByte)) if (thisByte ^ thatByte) != 0 => Diff.Binary(thisByte ^ thatByte) + case (Some(_), Some(_)) => Diff.Identical + case (None, Some(thatByte)) => Diff.Total(thatByte, Diff.Tag.Right) + case (Some(thisByte), None) => Diff.Total(thisByte, Diff.Tag.Left) + } + }.orIdentical + + def bool: Differ[Boolean] = + (thisBool: Boolean, thatBool: Boolean) => Diff.Bool(thisBool ^ thatBool) + + def numeric[A](implicit numeric: Numeric[A]): Differ[A] = + (thisValue: A, thatValue: A) => + numeric.minus(thisValue, thatValue) match { + case distance if distance == numeric.zero => Diff.Identical + case distance => Diff.Number(distance) + } + + def temporalAmount[A <: TemporalAmount](units: TemporalUnit): Differ[A] = + (thisA: A, thatA: A) => Diff.TemporalAmount(thisA.get(units) - thatA.get(units), units) + + def temporal[A <: JTemporal](units: ChronoUnit): Differ[A] = + (thisA: A, thatA: A) => Diff.Temporal(units.between(thisA, thatA), units) + + val dayOfWeek: Differ[DayOfWeek] = + (thisDay: DayOfWeek, thatDay: DayOfWeek) => + if (thisDay == thatDay) + Diff.Identical + else + Diff.Temporal((thatDay.getValue - thisDay.getValue).toLong, ChronoUnit.DAYS) + + val month: Differ[JMonth] = + (thisMonth: JMonth, thatMonth: JMonth) => + if (thisMonth == thatMonth) + Diff.Identical + else + Diff.Temporal((thatMonth.getValue - thisMonth.getValue).toLong, ChronoUnit.MONTHS) + + val monthDay: Differ[MonthDay] = + (thisMonthDay: MonthDay, thatMonthDay: MonthDay) => + if (thisMonthDay == thatMonthDay) + Diff.Identical + else + Diff.MonthDays( + ChronoUnit.DAYS.between(thisMonthDay.atYear(2001), thatMonthDay.atYear(2001)).toInt, + ChronoUnit.DAYS.between(thisMonthDay.atYear(2000), thatMonthDay.atYear(2000)).toInt + ) + + val bigInt: Differ[BigInteger] = + (thisValue: BigInteger, thatValue: BigInteger) => + thisValue.subtract(thatValue) match { + case BigInteger.ZERO => Diff.Identical + case distance => Diff.BigInt(distance) + } + + val bigDecimal: Differ[java.math.BigDecimal] = + (thisValue: java.math.BigDecimal, thatValue: java.math.BigDecimal) => + thisValue.subtract(thatValue) match { + case d if d.compareTo(java.math.BigDecimal.ZERO) == 0 => Diff.Identical + case distance => Diff.BigDecimal(distance) + } + + def tuple[A, B](left: Differ[A], right: Differ[B]): Differ[(A, B)] = + (thisValue: (A, B), thatValue: (A, B)) => + (thisValue, thatValue) match { + case ((thisA, thisB), (thatA, thatB)) => + left(thisA, thatA) <*> right(thisB, thatB) + } + + def either[A, B](left: Differ[A], right: Differ[B]) = + instancePartial[Either[A, B]] { + case (Left(l), Left(r)) => left(l, r) + case (Right(l), Right(r)) => right(l, r) + } + + def identical[A]: Differ[A] = (_: A, _: A) => Diff.Identical + + def fail[A]: Differ[A] = (_: A, _: A) => Diff.NotComparable + + def record(structure: Chunk[Schema.Field[_]]): Differ[ListMap[String, _]] = + (thisValue: ListMap[String, _], thatValue: ListMap[String, _]) => + if (!(conformsToStructure(thisValue, structure) && conformsToStructure(thatValue, structure))) + Diff.NotComparable + else + Diff + .Record( + ListMap.empty ++ thisValue.toList.zip(thatValue.toList).zipWithIndex.map { + case (((thisKey, thisValue), (_, thatValue)), fieldIndex) => + thisKey -> fromSchema(structure(fieldIndex).schema) + .asInstanceOf[Differ[Any]] + .apply(thisValue, thatValue) + } + ) + .orIdentical + + private def conformsToStructure(map: ListMap[String, _], structure: Chunk[Schema.Field[_]]): Boolean = + structure.foldRight(true) { + case (_, false) => false + case (field: Schema.Field[a], _) => map.get(field.label).map(_.isInstanceOf[a]).getOrElse(false) + } + + def enum[Z](cases: Schema.Case[_ <: Z, Z]*): Differ[Z] = + (thisZ: Z, thatZ: Z) => + cases + .foldRight[Option[Diff]](None) { + case (_, diff @ Some(_)) => diff + case (subtype, _) => + subtype.deconstruct(thisZ) -> (subtype.deconstruct(thatZ)) match { + case (Some(thisA), Some(thatA)) => + Some(fromSchema(subtype.codec)(thisA, thatA)) + case _ => None + } + } + .getOrElse(Diff.NotComparable) + + def enumeration(structure: ListMap[String, Schema[_]]): Differ[(String, _)] = + instancePartial[(String, _)] { + case ((thisKey, thisValue), (thatKey, thatValue)) if thisKey == thatKey => + structure + .get(thisKey) + .map(fromSchema(_).asInstanceOf[Differ[Any]].apply(thisValue, thatValue)) + .getOrElse(Diff.NotComparable) + } + + val string: Differ[String] = MyersDiff + + def instancePartial[A](f: PartialFunction[(A, A), Diff]) = + new Differ[A] { + override def apply(thisValue: A, thatValue: A): Diff = + f.applyOrElse((thisValue, thatValue), (_: (A, A)) => Diff.NotComparable) + } + +} + +sealed trait Diff { self => + + /** + * A symbolic operator for [[zip]]. + */ + def <*>(that: Diff): Diff = self.zip(that) + + def zip(that: Diff): Diff = Diff.Tuple(self, that) +} + +object Diff { + final case object Identical extends Diff + + final case class Binary(xor: Int) extends Diff + + final case class Bool(xor: Boolean) extends Diff + + final case class Number[A: Numeric](distance: A) extends Diff + + final case class BigInt(distance: BigInteger) extends Diff + + final case class BigDecimal(distance: java.math.BigDecimal) extends Diff + + final case class Temporal(distance: Long, timeUnit: TemporalUnit) extends Diff + + final case class TemporalAmount(difference: Long, units: TemporalUnit) extends Diff + + final case class MonthDays(difference: Int, leapYearDifference: Int) extends Diff + + final case class Tuple(leftDifference: Diff, rightDifference: Diff) extends Diff + + final case class Myers(edits: Chunk[Edit]) extends Diff + + final case class Total[A](value: A, tag: Tag) extends Diff + + /** + * Represents diff between incomparable values. For instance Left(1) and Right("a") + */ + case object NotComparable extends Diff + + /** + * Diff between two sequence of elements. The length of differences will be + * the length of the longest list. + * + * If both this and that have an element at index i then the ith element + * of difference will contain the diff between those elements + * + * If either this or that do not have an element at index i then the ith element + * of differences will be a total diff with the element and a tag representing which + * input was missing the ith index. + */ + final case class Sequence(differences: Chunk[Diff]) extends Diff { self => + + def orIdentical: Diff = + if (differences.forall(_ == Diff.Identical)) + Diff.Identical + else + self + } + + /** + * Set of elements which differ between two sets. + */ + final case class Set[A](differences: Set[Total[A]]) extends Diff + + /** + * Map of field-level diffs between two records. The map of differences + * is keyed to the records field names. + */ + final case class Record(differences: ListMap[String, Diff]) extends Diff { self => + + def orIdentical: Diff = + if (differences.values.forall(_ == Diff.Identical)) + Diff.Identical + else + self + } + + sealed trait Tag + + object Tag { + case object Left extends Tag + case object Right extends Tag + } + + sealed trait Edit + + object Edit { + final case class Delete(s: String) extends Edit + final case class Insert(s: String) extends Edit + final case class Keep(s: String) extends Edit + } +} + +object ProductDiffer { + + def unapply[A](schema: Schema[A]): Option[Differ[A]] = schema match { + case Schema.CaseObject(_) => Some(Differ.identical[A]) + case s: Schema.CaseClass1[_, A] => Some(product1(s)) + case s: Schema.CaseClass2[_, _, A] => Some(product2(s)) + case s: Schema.CaseClass3[_, _, _, A] => Some(product3(s)) + case s: Schema.CaseClass4[_, _, _, _, A] => Some(product4(s)) + case s: Schema.CaseClass5[_, _, _, _, _, A] => Some(product5(s)) + case s: Schema.CaseClass6[_, _, _, _, _, _, A] => Some(product6(s)) + case s: Schema.CaseClass7[_, _, _, _, _, _, _, A] => Some(product7(s)) + case s: Schema.CaseClass8[_, _, _, _, _, _, _, _, A] => Some(product8(s)) + case s: Schema.CaseClass9[_, _, _, _, _, _, _, _, _, A] => Some(product9(s)) + case s: Schema.CaseClass10[_, _, _, _, _, _, _, _, _, _, A] => Some(product10(s)) + case s: Schema.CaseClass11[_, _, _, _, _, _, _, _, _, _, _, A] => Some(product11(s)) + case s: Schema.CaseClass12[_, _, _, _, _, _, _, _, _, _, _, _, A] => Some(product12(s)) + case s: Schema.CaseClass13[_, _, _, _, _, _, _, _, _, _, _, _, _, A] => Some(product13(s)) + case s: Schema.CaseClass14[_, _, _, _, _, _, _, _, _, _, _, _, _, _, A] => Some(product14(s)) + case s: Schema.CaseClass15[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, A] => Some(product15(s)) + case s: Schema.CaseClass16[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, A] => Some(product16(s)) + case s: Schema.CaseClass17[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, A] => Some(product17(s)) + case s: Schema.CaseClass18[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, A] => Some(product18(s)) + case s: Schema.CaseClass19[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, A] => Some(product19(s)) + case s: Schema.CaseClass20[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, A] => Some(product20(s)) + case s: Schema.CaseClass21[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, A] => Some(product21(s)) + case s: Schema.CaseClass22[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, A] => + Some(product22(s)) + case _ => None + } + + def product1[A, Z](schema: Schema.CaseClass1[A, Z]): Differ[Z] = + (thisZ: Z, thatZ: Z) => + Diff + .Record( + ListMap( + fieldDiffer(schema.field, schema.extractField)(thisZ, thatZ) + ) + ) + .orIdentical + + def product2[A1, A2, Z](schema: Schema.CaseClass2[A1, A2, Z]): Differ[Z] = + (thisZ: Z, thatZ: Z) => + Diff + .Record( + ListMap.empty ++ Chunk( + fieldDiffer(schema.field1, schema.extractField1), + fieldDiffer(schema.field2, schema.extractField2) + ).map(_.apply(thisZ, thatZ)) + ) + .orIdentical + + def product3[A1, A2, A3, Z](schema: Schema.CaseClass3[A1, A2, A3, Z]): Differ[Z] = + (thisZ: Z, thatZ: Z) => + Diff + .Record( + ListMap.empty ++ Chunk( + fieldDiffer(schema.field1, schema.extractField1), + fieldDiffer(schema.field2, schema.extractField2), + fieldDiffer(schema.field3, schema.extractField3) + ).map(_.apply(thisZ, thatZ)) + ) + .orIdentical + + def product4[A1, A2, A3, A4, Z](schema: Schema.CaseClass4[A1, A2, A3, A4, Z]): Differ[Z] = + (thisZ: Z, thatZ: Z) => + Diff + .Record( + ListMap.empty ++ Chunk( + fieldDiffer(schema.field1, schema.extractField1), + fieldDiffer(schema.field2, schema.extractField2), + fieldDiffer(schema.field3, schema.extractField3), + fieldDiffer(schema.field4, schema.extractField4) + ).map(_.apply(thisZ, thatZ)) + ) + .orIdentical + + def product5[A1, A2, A3, A4, A5, Z](schema: Schema.CaseClass5[A1, A2, A3, A4, A5, Z]): Differ[Z] = + (thisZ: Z, thatZ: Z) => + Diff + .Record( + ListMap.empty ++ Chunk( + fieldDiffer(schema.field1, schema.extractField1), + fieldDiffer(schema.field2, schema.extractField2), + fieldDiffer(schema.field3, schema.extractField3), + fieldDiffer(schema.field4, schema.extractField4), + fieldDiffer(schema.field5, schema.extractField5) + ).map(_.apply(thisZ, thatZ)) + ) + .orIdentical + + def product6[A1, A2, A3, A4, A5, A6, Z](schema: Schema.CaseClass6[A1, A2, A3, A4, A5, A6, Z]): Differ[Z] = + (thisZ: Z, thatZ: Z) => + Diff + .Record( + ListMap.empty ++ Chunk( + fieldDiffer(schema.field1, schema.extractField1), + fieldDiffer(schema.field2, schema.extractField2), + fieldDiffer(schema.field3, schema.extractField3), + fieldDiffer(schema.field4, schema.extractField4), + fieldDiffer(schema.field5, schema.extractField5), + fieldDiffer(schema.field6, schema.extractField6) + ).map(_.apply(thisZ, thatZ)) + ) + .orIdentical + + def product7[A1, A2, A3, A4, A5, A6, A7, Z](schema: Schema.CaseClass7[A1, A2, A3, A4, A5, A6, A7, Z]): Differ[Z] = + (thisZ: Z, thatZ: Z) => + Diff + .Record( + ListMap.empty ++ Chunk( + fieldDiffer(schema.field1, schema.extractField1), + fieldDiffer(schema.field2, schema.extractField2), + fieldDiffer(schema.field3, schema.extractField3), + fieldDiffer(schema.field4, schema.extractField4), + fieldDiffer(schema.field5, schema.extractField5), + fieldDiffer(schema.field6, schema.extractField6), + fieldDiffer(schema.field7, schema.extractField7) + ).map(_.apply(thisZ, thatZ)) + ) + .orIdentical + + def product8[A1, A2, A3, A4, A5, A6, A7, A8, Z]( + schema: Schema.CaseClass8[A1, A2, A3, A4, A5, A6, A7, A8, Z] + ): Differ[Z] = + (thisZ: Z, thatZ: Z) => + Diff + .Record( + ListMap.empty ++ Chunk( + fieldDiffer(schema.field1, schema.extractField1), + fieldDiffer(schema.field2, schema.extractField2), + fieldDiffer(schema.field3, schema.extractField3), + fieldDiffer(schema.field4, schema.extractField4), + fieldDiffer(schema.field5, schema.extractField5), + fieldDiffer(schema.field6, schema.extractField6), + fieldDiffer(schema.field7, schema.extractField7), + fieldDiffer(schema.field8, schema.extractField8) + ).map(_.apply(thisZ, thatZ)) + ) + .orIdentical + + def product9[A1, A2, A3, A4, A5, A6, A7, A8, A9, Z]( + schema: Schema.CaseClass9[A1, A2, A3, A4, A5, A6, A7, A8, A9, Z] + ): Differ[Z] = + (thisZ: Z, thatZ: Z) => + Diff + .Record( + ListMap.empty ++ Chunk( + fieldDiffer(schema.field1, schema.extractField1), + fieldDiffer(schema.field2, schema.extractField2), + fieldDiffer(schema.field3, schema.extractField3), + fieldDiffer(schema.field4, schema.extractField4), + fieldDiffer(schema.field5, schema.extractField5), + fieldDiffer(schema.field6, schema.extractField6), + fieldDiffer(schema.field7, schema.extractField7), + fieldDiffer(schema.field8, schema.extractField8), + fieldDiffer(schema.field9, schema.extractField9) + ).map(_.apply(thisZ, thatZ)) + ) + .orIdentical + + def product10[A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, Z]( + schema: Schema.CaseClass10[A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, Z] + ): Differ[Z] = + (thisZ: Z, thatZ: Z) => + Diff + .Record( + ListMap.empty ++ Chunk( + fieldDiffer(schema.field1, schema.extractField1), + fieldDiffer(schema.field2, schema.extractField2), + fieldDiffer(schema.field3, schema.extractField3), + fieldDiffer(schema.field4, schema.extractField4), + fieldDiffer(schema.field5, schema.extractField5), + fieldDiffer(schema.field6, schema.extractField6), + fieldDiffer(schema.field7, schema.extractField7), + fieldDiffer(schema.field8, schema.extractField8), + fieldDiffer(schema.field9, schema.extractField9), + fieldDiffer(schema.field10, schema.extractField10) + ).map(_.apply(thisZ, thatZ)) + ) + .orIdentical + + def product11[A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, Z]( + schema: Schema.CaseClass11[A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, Z] + ): Differ[Z] = + (thisZ: Z, thatZ: Z) => + Diff + .Record( + ListMap.empty ++ Chunk( + fieldDiffer(schema.field1, schema.extractField1), + fieldDiffer(schema.field2, schema.extractField2), + fieldDiffer(schema.field3, schema.extractField3), + fieldDiffer(schema.field4, schema.extractField4), + fieldDiffer(schema.field5, schema.extractField5), + fieldDiffer(schema.field6, schema.extractField6), + fieldDiffer(schema.field7, schema.extractField7), + fieldDiffer(schema.field8, schema.extractField8), + fieldDiffer(schema.field9, schema.extractField9), + fieldDiffer(schema.field10, schema.extractField10), + fieldDiffer(schema.field11, schema.extractField11) + ).map(_.apply(thisZ, thatZ)) + ) + .orIdentical + + def product12[A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, Z]( + schema: Schema.CaseClass12[A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, Z] + ): Differ[Z] = + (thisZ: Z, thatZ: Z) => + Diff + .Record( + ListMap.empty ++ Chunk( + fieldDiffer(schema.field1, schema.extractField1), + fieldDiffer(schema.field2, schema.extractField2), + fieldDiffer(schema.field3, schema.extractField3), + fieldDiffer(schema.field4, schema.extractField4), + fieldDiffer(schema.field5, schema.extractField5), + fieldDiffer(schema.field6, schema.extractField6), + fieldDiffer(schema.field7, schema.extractField7), + fieldDiffer(schema.field8, schema.extractField8), + fieldDiffer(schema.field9, schema.extractField9), + fieldDiffer(schema.field10, schema.extractField10), + fieldDiffer(schema.field11, schema.extractField11), + fieldDiffer(schema.field12, schema.extractField12) + ).map(_.apply(thisZ, thatZ)) + ) + .orIdentical + + def product13[A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, Z]( + schema: Schema.CaseClass13[A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, Z] + ): Differ[Z] = + (thisZ: Z, thatZ: Z) => + Diff + .Record( + ListMap.empty ++ Chunk( + fieldDiffer(schema.field1, schema.extractField1), + fieldDiffer(schema.field2, schema.extractField2), + fieldDiffer(schema.field3, schema.extractField3), + fieldDiffer(schema.field4, schema.extractField4), + fieldDiffer(schema.field5, schema.extractField5), + fieldDiffer(schema.field6, schema.extractField6), + fieldDiffer(schema.field7, schema.extractField7), + fieldDiffer(schema.field8, schema.extractField8), + fieldDiffer(schema.field9, schema.extractField9), + fieldDiffer(schema.field10, schema.extractField10), + fieldDiffer(schema.field11, schema.extractField11), + fieldDiffer(schema.field12, schema.extractField12), + fieldDiffer(schema.field13, schema.extractField13) + ).map(_.apply(thisZ, thatZ)) + ) + .orIdentical + + def product14[A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, Z]( + schema: Schema.CaseClass14[A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, Z] + ): Differ[Z] = + (thisZ: Z, thatZ: Z) => + Diff + .Record( + ListMap.empty ++ Chunk( + fieldDiffer(schema.field1, schema.extractField1), + fieldDiffer(schema.field2, schema.extractField2), + fieldDiffer(schema.field3, schema.extractField3), + fieldDiffer(schema.field4, schema.extractField4), + fieldDiffer(schema.field5, schema.extractField5), + fieldDiffer(schema.field6, schema.extractField6), + fieldDiffer(schema.field7, schema.extractField7), + fieldDiffer(schema.field8, schema.extractField8), + fieldDiffer(schema.field9, schema.extractField9), + fieldDiffer(schema.field10, schema.extractField10), + fieldDiffer(schema.field11, schema.extractField11), + fieldDiffer(schema.field12, schema.extractField12), + fieldDiffer(schema.field13, schema.extractField13), + fieldDiffer(schema.field14, schema.extractField14) + ).map(_.apply(thisZ, thatZ)) + ) + .orIdentical + + def product15[A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, Z]( + schema: Schema.CaseClass15[A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, Z] + ): Differ[Z] = + (thisZ: Z, thatZ: Z) => + Diff + .Record( + ListMap.empty ++ Chunk( + fieldDiffer(schema.field1, schema.extractField1), + fieldDiffer(schema.field2, schema.extractField2), + fieldDiffer(schema.field3, schema.extractField3), + fieldDiffer(schema.field4, schema.extractField4), + fieldDiffer(schema.field5, schema.extractField5), + fieldDiffer(schema.field6, schema.extractField6), + fieldDiffer(schema.field7, schema.extractField7), + fieldDiffer(schema.field8, schema.extractField8), + fieldDiffer(schema.field9, schema.extractField9), + fieldDiffer(schema.field10, schema.extractField10), + fieldDiffer(schema.field11, schema.extractField11), + fieldDiffer(schema.field12, schema.extractField12), + fieldDiffer(schema.field13, schema.extractField13), + fieldDiffer(schema.field14, schema.extractField14), + fieldDiffer(schema.field15, schema.extractField15) + ).map(_.apply(thisZ, thatZ)) + ) + .orIdentical + + def product16[A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, Z]( + schema: Schema.CaseClass16[A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, Z] + ): Differ[Z] = + (thisZ: Z, thatZ: Z) => + Diff + .Record( + ListMap.empty ++ Chunk( + fieldDiffer(schema.field1, schema.extractField1), + fieldDiffer(schema.field2, schema.extractField2), + fieldDiffer(schema.field3, schema.extractField3), + fieldDiffer(schema.field4, schema.extractField4), + fieldDiffer(schema.field5, schema.extractField5), + fieldDiffer(schema.field6, schema.extractField6), + fieldDiffer(schema.field7, schema.extractField7), + fieldDiffer(schema.field8, schema.extractField8), + fieldDiffer(schema.field9, schema.extractField9), + fieldDiffer(schema.field10, schema.extractField10), + fieldDiffer(schema.field11, schema.extractField11), + fieldDiffer(schema.field12, schema.extractField12), + fieldDiffer(schema.field13, schema.extractField13), + fieldDiffer(schema.field14, schema.extractField14), + fieldDiffer(schema.field15, schema.extractField15), + fieldDiffer(schema.field16, schema.extractField16) + ).map(_.apply(thisZ, thatZ)) + ) + .orIdentical + + def product17[A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, Z]( + schema: Schema.CaseClass17[A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, Z] + ): Differ[Z] = + (thisZ: Z, thatZ: Z) => + Diff + .Record( + ListMap.empty ++ Chunk( + fieldDiffer(schema.field1, schema.extractField1), + fieldDiffer(schema.field2, schema.extractField2), + fieldDiffer(schema.field3, schema.extractField3), + fieldDiffer(schema.field4, schema.extractField4), + fieldDiffer(schema.field5, schema.extractField5), + fieldDiffer(schema.field6, schema.extractField6), + fieldDiffer(schema.field7, schema.extractField7), + fieldDiffer(schema.field8, schema.extractField8), + fieldDiffer(schema.field9, schema.extractField9), + fieldDiffer(schema.field10, schema.extractField10), + fieldDiffer(schema.field11, schema.extractField11), + fieldDiffer(schema.field12, schema.extractField12), + fieldDiffer(schema.field13, schema.extractField13), + fieldDiffer(schema.field14, schema.extractField14), + fieldDiffer(schema.field15, schema.extractField15), + fieldDiffer(schema.field16, schema.extractField16), + fieldDiffer(schema.field17, schema.extractField17) + ).map(_.apply(thisZ, thatZ)) + ) + .orIdentical + + def product18[A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, Z]( + schema: Schema.CaseClass18[A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, Z] + ): Differ[Z] = + (thisZ: Z, thatZ: Z) => + Diff + .Record( + ListMap.empty ++ Chunk( + fieldDiffer(schema.field1, schema.extractField1), + fieldDiffer(schema.field2, schema.extractField2), + fieldDiffer(schema.field3, schema.extractField3), + fieldDiffer(schema.field4, schema.extractField4), + fieldDiffer(schema.field5, schema.extractField5), + fieldDiffer(schema.field6, schema.extractField6), + fieldDiffer(schema.field7, schema.extractField7), + fieldDiffer(schema.field8, schema.extractField8), + fieldDiffer(schema.field9, schema.extractField9), + fieldDiffer(schema.field10, schema.extractField10), + fieldDiffer(schema.field11, schema.extractField11), + fieldDiffer(schema.field12, schema.extractField12), + fieldDiffer(schema.field13, schema.extractField13), + fieldDiffer(schema.field14, schema.extractField14), + fieldDiffer(schema.field15, schema.extractField15), + fieldDiffer(schema.field16, schema.extractField16), + fieldDiffer(schema.field17, schema.extractField17), + fieldDiffer(schema.field18, schema.extractField18) + ).map(_.apply(thisZ, thatZ)) + ) + .orIdentical + + def product19[A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, Z]( + schema: Schema.CaseClass19[A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, Z] + ): Differ[Z] = + (thisZ: Z, thatZ: Z) => + Diff + .Record( + ListMap.empty ++ Chunk( + fieldDiffer(schema.field1, schema.extractField1), + fieldDiffer(schema.field2, schema.extractField2), + fieldDiffer(schema.field3, schema.extractField3), + fieldDiffer(schema.field4, schema.extractField4), + fieldDiffer(schema.field5, schema.extractField5), + fieldDiffer(schema.field6, schema.extractField6), + fieldDiffer(schema.field7, schema.extractField7), + fieldDiffer(schema.field8, schema.extractField8), + fieldDiffer(schema.field9, schema.extractField9), + fieldDiffer(schema.field10, schema.extractField10), + fieldDiffer(schema.field11, schema.extractField11), + fieldDiffer(schema.field12, schema.extractField12), + fieldDiffer(schema.field13, schema.extractField13), + fieldDiffer(schema.field14, schema.extractField14), + fieldDiffer(schema.field15, schema.extractField15), + fieldDiffer(schema.field16, schema.extractField16), + fieldDiffer(schema.field17, schema.extractField17), + fieldDiffer(schema.field18, schema.extractField18), + fieldDiffer(schema.field19, schema.extractField19) + ).map(_.apply(thisZ, thatZ)) + ) + .orIdentical + + def product20[A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, Z]( + schema: Schema.CaseClass20[ + A1, + A2, + A3, + A4, + A5, + A6, + A7, + A8, + A9, + A10, + A11, + A12, + A13, + A14, + A15, + A16, + A17, + A18, + A19, + A20, + Z + ] + ): Differ[Z] = + (thisZ: Z, thatZ: Z) => + Diff + .Record( + ListMap.empty ++ Chunk( + fieldDiffer(schema.field1, schema.extractField1), + fieldDiffer(schema.field2, schema.extractField2), + fieldDiffer(schema.field3, schema.extractField3), + fieldDiffer(schema.field4, schema.extractField4), + fieldDiffer(schema.field5, schema.extractField5), + fieldDiffer(schema.field6, schema.extractField6), + fieldDiffer(schema.field7, schema.extractField7), + fieldDiffer(schema.field8, schema.extractField8), + fieldDiffer(schema.field9, schema.extractField9), + fieldDiffer(schema.field10, schema.extractField10), + fieldDiffer(schema.field11, schema.extractField11), + fieldDiffer(schema.field12, schema.extractField12), + fieldDiffer(schema.field13, schema.extractField13), + fieldDiffer(schema.field14, schema.extractField14), + fieldDiffer(schema.field15, schema.extractField15), + fieldDiffer(schema.field16, schema.extractField16), + fieldDiffer(schema.field17, schema.extractField17), + fieldDiffer(schema.field18, schema.extractField18), + fieldDiffer(schema.field19, schema.extractField19), + fieldDiffer(schema.field20, schema.extractField20) + ).map(_.apply(thisZ, thatZ)) + ) + .orIdentical + + def product21[A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21, Z]( + schema: Schema.CaseClass21[ + A1, + A2, + A3, + A4, + A5, + A6, + A7, + A8, + A9, + A10, + A11, + A12, + A13, + A14, + A15, + A16, + A17, + A18, + A19, + A20, + A21, + Z + ] + ): Differ[Z] = + (thisZ: Z, thatZ: Z) => + Diff + .Record( + ListMap.empty ++ Chunk( + fieldDiffer(schema.field1, schema.extractField1), + fieldDiffer(schema.field2, schema.extractField2), + fieldDiffer(schema.field3, schema.extractField3), + fieldDiffer(schema.field4, schema.extractField4), + fieldDiffer(schema.field5, schema.extractField5), + fieldDiffer(schema.field6, schema.extractField6), + fieldDiffer(schema.field7, schema.extractField7), + fieldDiffer(schema.field8, schema.extractField8), + fieldDiffer(schema.field9, schema.extractField9), + fieldDiffer(schema.field10, schema.extractField10), + fieldDiffer(schema.field11, schema.extractField11), + fieldDiffer(schema.field12, schema.extractField12), + fieldDiffer(schema.field13, schema.extractField13), + fieldDiffer(schema.field14, schema.extractField14), + fieldDiffer(schema.field15, schema.extractField15), + fieldDiffer(schema.field16, schema.extractField16), + fieldDiffer(schema.field17, schema.extractField17), + fieldDiffer(schema.field18, schema.extractField18), + fieldDiffer(schema.field19, schema.extractField19), + fieldDiffer(schema.field20, schema.extractField20), + fieldDiffer(schema.field21, schema.extractField21) + ).map(_.apply(thisZ, thatZ)) + ) + .orIdentical + + def product22[A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21, A22, Z]( + schema: Schema.CaseClass22[ + A1, + A2, + A3, + A4, + A5, + A6, + A7, + A8, + A9, + A10, + A11, + A12, + A13, + A14, + A15, + A16, + A17, + A18, + A19, + A20, + A21, + A22, + Z + ] + ): Differ[Z] = + (thisZ: Z, thatZ: Z) => + Diff + .Record( + ListMap.empty ++ Chunk( + fieldDiffer(schema.field1, schema.extractField1), + fieldDiffer(schema.field2, schema.extractField2), + fieldDiffer(schema.field3, schema.extractField3), + fieldDiffer(schema.field4, schema.extractField4), + fieldDiffer(schema.field5, schema.extractField5), + fieldDiffer(schema.field6, schema.extractField6), + fieldDiffer(schema.field7, schema.extractField7), + fieldDiffer(schema.field8, schema.extractField8), + fieldDiffer(schema.field9, schema.extractField9), + fieldDiffer(schema.field10, schema.extractField10), + fieldDiffer(schema.field11, schema.extractField11), + fieldDiffer(schema.field12, schema.extractField12), + fieldDiffer(schema.field13, schema.extractField13), + fieldDiffer(schema.field14, schema.extractField14), + fieldDiffer(schema.field15, schema.extractField15), + fieldDiffer(schema.field16, schema.extractField16), + fieldDiffer(schema.field17, schema.extractField17), + fieldDiffer(schema.field18, schema.extractField18), + fieldDiffer(schema.field19, schema.extractField19), + fieldDiffer(schema.field20, schema.extractField20), + fieldDiffer(schema.field21, schema.extractField21), + fieldDiffer(schema.field22, schema.extractField22) + ).map(_.apply(thisZ, thatZ)) + ) + .orIdentical + + private def fieldDiffer[A, Z](field: Schema.Field[A], extract: Z => A): (Z, Z) => (String, Diff) = + (thisZ: Z, thatZ: Z) => field.label -> Differ.fromSchema(field.schema)(extract(thisZ), extract(thatZ)) +} diff --git a/zio-schema/shared/src/main/scala/zio/schema/Schema.scala b/zio-schema/shared/src/main/scala/zio/schema/Schema.scala index 4a65bef43..5da7edc4e 100644 --- a/zio-schema/shared/src/main/scala/zio/schema/Schema.scala +++ b/zio-schema/shared/src/main/scala/zio/schema/Schema.scala @@ -43,6 +43,17 @@ sealed trait Schema[A] { */ def <+>[B](that: Schema[B]): Schema[Either[A, B]] = self.orElseEither(that) + /** + * Performs a diff between thisValue and thatValue. See [[zio.schema.Differ]] for details + * on the default diff algorithms. + * + * A custom [[zio.schema.Differ]] can be supplied if the default behavior is not acceptable. + */ + def diff(thisValue: A, thatValue: A, differ: Option[Differ[A]] = None): Diff = differ match { + case Some(differ) => differ(thisValue, thatValue) + case None => Differ.fromSchema(self)(thisValue, thatValue) + } + def fromDynamic(value: DynamicValue): Either[String, A] = value.toTypedValue(self) @@ -79,7 +90,6 @@ sealed trait Schema[A] { * their tuple composition. */ def zip[B](that: Schema[B]): Schema[(A, B)] = Schema.Tuple(self, that) - } object Schema { @@ -876,7 +886,7 @@ object Schema { final case class Enum2[A1 <: Z, A2 <: Z, Z](case1: Case[A1, Z], case2: Case[A2, Z]) extends Schema[Z] final case class Enum3[A1 <: Z, A2 <: Z, A3 <: Z, Z](case1: Case[A1, Z], case2: Case[A2, Z], case3: Case[A3, Z]) extends Schema[Z] - final case class EnumN[Z](cases: Seq[Case[_, Z]]) extends Schema[Z] + final case class EnumN[Z](cases: Seq[Case[_ <: Z, Z]]) extends Schema[Z] final case class CaseObject[Z](instance: Z) extends Schema[Z] diff --git a/zio-schema/shared/src/main/scala/zio/schema/internal/MyersDiff.scala b/zio-schema/shared/src/main/scala/zio/schema/internal/MyersDiff.scala new file mode 100644 index 000000000..ddc87586c --- /dev/null +++ b/zio-schema/shared/src/main/scala/zio/schema/internal/MyersDiff.scala @@ -0,0 +1,122 @@ +package zio.schema.internal + +import zio.Chunk +import zio.schema.{ Diff, Differ } + +object MyersDiff extends Differ[String] { + + def apply(original: String, modified: String): Diff = { + + var varOriginal = original + var varModified = modified + var longestCommonSubstring: String = getLongestCommonSubsequence(original, modified) + + var edits: Chunk[Diff.Edit] = Chunk.empty + + while (longestCommonSubstring.size > 0) { + val headOfLongestCommonSubstring = longestCommonSubstring(0) + longestCommonSubstring = longestCommonSubstring.drop(1) + + var headOfModified = varModified(0) + var loop = true + + while (loop) { + headOfModified = varModified(0) + varModified = varModified.drop(1) + if (headOfModified != headOfLongestCommonSubstring) + edits = edits :+ Diff.Edit.Insert(headOfModified.toString) + + loop = varModified.size > 0 && headOfModified != headOfLongestCommonSubstring + } + + var headOfOriginal = varOriginal(0) + loop = true + + while (loop) { + headOfOriginal = varOriginal(0) + varOriginal = varOriginal.drop(1) + if (headOfOriginal != headOfLongestCommonSubstring) + edits = edits :+ Diff.Edit.Delete(headOfOriginal.toString) + + loop = varOriginal.size > 0 && headOfOriginal != headOfLongestCommonSubstring + } + + edits = edits :+ Diff.Edit.Keep(headOfLongestCommonSubstring.toString) + } + + while (varModified.size > 0) { + val headOfModified = varModified(0) + varModified = varModified.drop(1) + edits = edits :+ Diff.Edit.Insert(headOfModified.toString) + } + + while (varOriginal.size > 0) { + val headOfOriginal = varOriginal(0) + varOriginal = varOriginal.drop(1) + edits = edits :+ Diff.Edit.Delete(headOfOriginal.toString) + } + + if (isIdentical(edits)) Diff.Identical else Diff.Myers(edits) + } + + private def isIdentical(edits: Chunk[Diff.Edit]): Boolean = + edits.isEmpty || edits.forall { + case Diff.Edit.Keep(_) => true + case _ => false + } + + def getLongestCommonSubsequence(original: String, modified: String): String = + if (original == null || original.length() == 0 || modified == null || modified.length() == 0) "" + else if (original == modified) original + else { + + val myersMatrix: Array[Array[Int]] = initializeMyersMatrix(original, modified) + val longestCommonSubsequence = new StringBuilder() + + var originalPosition = original.length() + var modifiedPosition = modified.length() + + var loop = true + + while (loop) { + if (myersMatrix(originalPosition)(modifiedPosition) == myersMatrix(originalPosition - 1)(modifiedPosition)) { + originalPosition -= 1 + } else if (myersMatrix(originalPosition)(modifiedPosition) == myersMatrix(originalPosition)( + modifiedPosition - 1 + )) { + modifiedPosition -= 1 + } else { + longestCommonSubsequence += original.charAt(originalPosition - 1) + originalPosition -= 1 + modifiedPosition -= 1 + } + + loop = originalPosition > 0 && modifiedPosition > 0 + } + + longestCommonSubsequence.toString.reverse + } + + private def initializeMyersMatrix(original: String, modified: String): Array[Array[Int]] = { + val originalLength = original.length() + val modifiedLength = modified.length() + + val myersMatrix = Array.fill[Int](originalLength + 1, modifiedLength + 1)(0) + + for (i <- 0 until originalLength) { + for (j <- 0 until modifiedLength) { + if (original.charAt(i) == modified.charAt(j)) { + myersMatrix(i + 1)(j + 1) = myersMatrix(i)(j) + 1 + } else { + if (myersMatrix(i)(j + 1) >= myersMatrix(i + 1)(j)) { + myersMatrix(i + 1)(j + 1) = myersMatrix(i)(j + 1) + } else { + myersMatrix(i + 1)(j + 1) = myersMatrix(i + 1)(j) + } + } + } + } + + myersMatrix + } +} diff --git a/zio-schema/shared/src/main/scala/zio/schema/syntax.scala b/zio-schema/shared/src/main/scala/zio/schema/syntax.scala new file mode 100644 index 000000000..db084f88c --- /dev/null +++ b/zio-schema/shared/src/main/scala/zio/schema/syntax.scala @@ -0,0 +1,18 @@ +package zio.schema + +object syntax extends SchemaSyntax + +trait SchemaSyntax { + implicit class DiffOps[A: Schema](a: A) { + def diff(that: A): Diff = Schema[A].diff(a, that) + + /** + * alias for diff that does not conflict with scala stdlib + */ + def diffEach(that: A): Diff = Schema[A].diff(a, that) + } + + implicit class DynamicValueOps[A: Schema](a: A) { + def dynamic: DynamicValue = Schema[A].toDynamic(a) + } +} diff --git a/zio-schema/shared/src/test/scala/zio/schema/DiffSpec.scala b/zio-schema/shared/src/test/scala/zio/schema/DiffSpec.scala new file mode 100644 index 000000000..0fa0d2e2e --- /dev/null +++ b/zio-schema/shared/src/test/scala/zio/schema/DiffSpec.scala @@ -0,0 +1,302 @@ +package zio.schema + +import java.math.BigInteger +import java.time.temporal.ChronoUnit +import java.time.{ DayOfWeek, MonthDay } + +import scala.collection.immutable.ListMap + +import zio.Chunk +import zio.random.Random +import zio.schema.SchemaGen.{ Arity1, Arity24 } +import zio.schema.syntax._ +import zio.test.{ DefaultRunnableSpec, Diff => _, _ } + +object DiffSpec extends DefaultRunnableSpec { + + def spec: ZSpec[Environment, Failure] = suite("Differ")( + suite("standard types")( + suite("binary")( + testM("same length") { + check(Gen.chunkOfN(10)(Gen.anyByte) <*> Gen.chunkOfN(10)(Gen.anyByte)) { + case (theseBytes, thoseBytes) => + val expected = + if (theseBytes == thoseBytes) + Diff.Identical + else + Diff.Sequence( + theseBytes + .zip(thoseBytes) + .map(b => b._1 ^ b._2) + .map { + case 0 => Diff.Identical + case i => Diff.Binary(i) + } + ) + + assertTrue(theseBytes.diffEach(thoseBytes) == expected) + } + }, + testM("that is longer") { + check(Gen.chunkOfN(10)(Gen.anyByte) <*> Gen.chunkOfN(12)(Gen.anyByte)) { + case (theseBytes, thoseBytes) => + val expected = + Diff.Sequence( + theseBytes + .zip(thoseBytes) + .map(b => b._1 ^ b._2) + .map { + case 0 => Diff.Identical + case i => Diff.Binary(i) + } ++ Chunk(Diff.Total(thoseBytes(10), Diff.Tag.Right), Diff.Total(thoseBytes(11), Diff.Tag.Right)) + ) + + assertTrue(theseBytes.diffEach(thoseBytes) == expected) + } + }, + testM("this is longer") { + check(Gen.chunkOfN(12)(Gen.anyByte) <*> Gen.chunkOfN(10)(Gen.anyByte)) { + case (theseBytes, thoseBytes) => + val expected = + Diff.Sequence( + theseBytes + .zip(thoseBytes) + .map(b => b._1 ^ b._2) + .map { + case 0 => Diff.Identical + case i => Diff.Binary(i) + } ++ Chunk(Diff.Total(theseBytes(10), Diff.Tag.Left), Diff.Total(theseBytes(11), Diff.Tag.Left)) + ) + + assertTrue(theseBytes.diffEach(thoseBytes) == expected) + } + } + ), + testM("int") { + check(Gen.anyInt <*> Gen.anyInt) { + case (left, right) => + assertTrue(left.diff(right) == Diff.Number(left - right)) + assertTrue(left.diff(left) == Diff.Identical) + } + }, + testM("double") { + check(Gen.anyDouble <*> Gen.anyDouble) { + case (left, right) => + assertTrue(left.diff(right) == Diff.Number(left - right)) + assertTrue(left.diff(left) == Diff.Identical) + } + }, + testM("float") { + check(Gen.anyFloat <*> Gen.anyFloat) { + case (left, right) => + assertTrue(left.diff(right) == Diff.Number(left - right)) + assertTrue(left.diff(left) == Diff.Identical) + } + }, + testM("long") { + check(Gen.anyLong <*> Gen.anyLong) { + case (left, right) => + assertTrue(left.diff(right) == Diff.Number(left - right)) + assertTrue(left.diff(left) == Diff.Identical) + } + }, + testM("short") { + check(Gen.anyShort <*> Gen.anyShort) { + case (left, right) => + assertTrue(left.diff(right) == Diff.Number(left - right)) + assertTrue(left.diff(left) == Diff.Identical) + } + }, + testM("BigInteger") { + check(bigIntegerGen <*> bigIntegerGen) { + case (left, right) => + assertTrue(left.diff(right) == Diff.BigInt(left.subtract(right))) + assertTrue(left.diff(left) == Diff.Identical) + } + }, + testM("BigDecimal") { + check(bigDecimalGen <*> bigDecimalGen) { + case (left, right) => + assertTrue(left.diff(right) == Diff.BigDecimal(left.subtract(right))) + assertTrue(left.diff(left) == Diff.Identical) + } + } + ), + suite("string")( + testM("identical") { + check(Gen.anyString) { s => + assertTrue(s.diffEach(s) == Diff.Identical) + } + }, + testM("append character") { + check(Gen.anyString <*> Gen.anyChar) { + case (str, ch) => + val expected = + Diff.Myers(Chunk.fromIterable(str.map(c => Diff.Edit.Keep(c.toString))) :+ Diff.Edit.Insert(ch.toString)) + assertTrue(str.diffEach(str + ch.toString) == expected) + } + } + ), + suite("temporal")( + testM("day of week") { + check(Gen.elements(1, 2, 3, 4, 5, 6, 7) <*> Gen.elements(1, 2, 3, 4, 5, 6, 7)) { + case (i1, i2) => + val expected = if (i1 == i2) Diff.Identical else Diff.Temporal((i2 - i1).toLong, ChronoUnit.DAYS) + assertTrue(DayOfWeek.of(i1).diff(DayOfWeek.of(i2)) == expected) + } + }, + testM("month") { + check(Gen.anyMonth <*> Gen.anyMonth) { + case (thisMonth, thatMonth) => + val expected = + if (thisMonth == thatMonth) Diff.Identical + else Diff.Temporal((thatMonth.getValue - thisMonth.getValue).toLong, ChronoUnit.MONTHS) + assertTrue(thisMonth.diff(thatMonth) == expected) + } + }, + suite("month day")( + test("leap year adjustment") { + val expected = Diff.MonthDays(1, 2) + val thisMonthDay = MonthDay.of(2, 28) + val thatMonthDay = MonthDay.of(3, 1) + assertTrue(thisMonthDay.diff(thatMonthDay) == expected) + }, + test("no leap year adjustment") { + val expected = Diff.MonthDays(-1, -1) + val thisMonthDay = MonthDay.of(2, 1) + val thatMonthDay = MonthDay.of(1, 31) + assertTrue(thisMonthDay.diff(thatMonthDay) == expected) + }, + testM("any") { + check(Gen.anyMonthDay <*> Gen.anyMonthDay) { + case (thisMonthDay, thatMonthDay) if thisMonthDay == thatMonthDay => + assertTrue(thisMonthDay.diff(thatMonthDay) == Diff.Identical) + case (thisMonthDay, thatMonthDay) => + val expected = Diff.MonthDays( + ChronoUnit.DAYS.between(thisMonthDay.atYear(2001), thatMonthDay.atYear(2001)).toInt, + ChronoUnit.DAYS.between(thisMonthDay.atYear(2000), thatMonthDay.atYear(2000)).toInt + ) + assertTrue(thisMonthDay.diff(thatMonthDay) == expected) + } + } + ) + ), + suite("collections")( + testM("list of primitives of equal length") { + check(Gen.listOfN(10)(Gen.anyInt) <*> Gen.listOfN(10)(Gen.anyInt)) { + case (ls, rs) => + val expected = Diff.Sequence( + Chunk + .fromIterable(ls.zip(rs).map(p => p._1 - p._2).map(d => if (d != 0) Diff.Number(d) else Diff.Identical)) + ) + assertTrue(Schema[List[Int]].diff(ls, rs) == expected) + assertTrue(Schema[List[Int]].diff(ls, ls) == Diff.Identical) + } + }, + testM("list of primitive where that list is longer") { + check(Gen.listOfN(10)(Gen.anyInt) <*> Gen.listOfN(12)(Gen.anyInt)) { + case (ls, rs) => + val expected = Diff.Sequence( + Chunk + .fromIterable( + ls.zip(rs).map(p => p._1 - p._2).map(d => if (d != 0) Diff.Number(d) else Diff.Identical) + ) ++ Chunk(Diff.Total(rs(10), Diff.Tag.Right), Diff.Total(rs(11), Diff.Tag.Right)) + ) + assertTrue(Schema[List[Int]].diff(ls, rs) == expected) + } + }, + testM("list of primitive where this list is longer") { + check(Gen.listOfN(12)(Gen.anyInt) <*> Gen.listOfN(10)(Gen.anyInt)) { + case (ls, rs) => + val expected = Diff.Sequence( + Chunk + .fromIterable( + ls.zip(rs).map(p => p._1 - p._2).map(d => if (d != 0) Diff.Number(d) else Diff.Identical) + ) ++ Chunk(Diff.Total(ls(10), Diff.Tag.Left), Diff.Total(ls(11), Diff.Tag.Left)) + ) + assertTrue(Schema[List[Int]].diff(ls, rs) == expected) + } + }, + testM("any list of primitives") { + check(Gen.chunkOf(Gen.anyInt) <*> Gen.chunkOf(Gen.anyInt)) { + case (ls, rs) => + val expected = + if (ls == rs) + Diff.Identical + else + Diff.Sequence(ls.zipAll(rs).map(p => p._1.diff(p._2))) + assertTrue(ls.diffEach(rs) == expected) + } + } + ), + suite("records")( + testM("records with invalid structure not be comparable") { + check(Gen.mapOf(Gen.anyString, Gen.anyInt) <*> Gen.mapOf(Gen.anyString, Gen.anyInt)) { + case (thisMap, thatMap) => + val diff = Schema + .GenericRecord(Chunk(Schema.Field("key", Schema[String]))) + .diff(ListMap.empty ++ thisMap, ListMap.empty ++ thatMap) + assertTrue(diff == Diff.NotComparable) + } + } + ), + suite("product type")( + testM("arity 1") { + check(Gen.anyInt) { i => + assertTrue(Arity1(i).diff(Arity1(i - 1)) == Diff.Record(ListMap("value" -> Diff.Number[Int](1)))) + } + }, + testM("arity 2") { + check(SchemaGen.anyArity2 <*> SchemaGen.anyArity2) { + case (thisA, thatA) => + val expected = + if (thisA == thatA) + Diff.Identical + else + Diff.Record( + ListMap("value1" -> thisA.value1.diffEach(thatA.value1), "value2" -> thisA.value2.diff(thatA.value2)) + ) + assertTrue(thisA.diff(thatA) == expected) + } + }, + testM("arity greater than 22") { + check(SchemaGen.anyArity24 <*> SchemaGen.anyArity24) { + case (thisA, thatA) => + val expected = + if (thisA == thatA) + Diff.Identical + else { + Diff.Record( + ListMap.empty ++ Schema[Arity24] + .asInstanceOf[Schema.Transform[ListMap[String, _], Arity24]] + .codec + .asInstanceOf[Schema.GenericRecord] + .structure + .zipWithIndex + .map { + case (field, index) => + field.label -> Differ + .fromSchema(field.schema) + .asInstanceOf[Differ[Any]]( + thisA.asInstanceOf[Product].productElement(index), + thatA.asInstanceOf[Product].productElement(index) + ) + } + .toList + ) + } + assertTrue(thisA.diff(thatA) == expected) + } + }, + testM("identical") { + check(SchemaGen.anyArity) { value => + assertTrue(value.diff(value) == Diff.Identical) + } + } + ) + ) + + val bigIntegerGen: Gen[Random, BigInteger] = Gen.anyLong.map(d => java.math.BigInteger.valueOf(d)) + val bigDecimalGen: Gen[Random, java.math.BigDecimal] = Gen.anyDouble.map(d => java.math.BigDecimal.valueOf(d)) + +} diff --git a/zio-schema/shared/src/test/scala/zio/schema/SchemaGen.scala b/zio-schema/shared/src/test/scala/zio/schema/SchemaGen.scala index f4e0883a2..30a1bd8fb 100644 --- a/zio-schema/shared/src/test/scala/zio/schema/SchemaGen.scala +++ b/zio-schema/shared/src/test/scala/zio/schema/SchemaGen.scala @@ -329,10 +329,22 @@ object SchemaGen { } yield (schema -> schema.fromDynamic(dynamic).toOption.get).asInstanceOf[SchemaAndValue[Any]] sealed trait Arity - case object Arity0 extends Arity - final case class Arity1(value: Int) extends Arity - final case class Arity2(value1: String, value2: Arity1) extends Arity + case object Arity0 extends Arity + final case class Arity1(value: Int) extends Arity + + object Arity1 { + implicit val schema: Schema[Arity1] = DeriveSchema.gen[Arity1] + } + final case class Arity2(value1: String, value2: Arity1) extends Arity + + object Arity2 { + implicit val schema: Schema[Arity2] = DeriveSchema.gen[Arity2] + } final case class Arity3(value1: String, value2: Arity2, value3: Arity1) extends Arity + + object Arity3 { + implicit val schema: Schema[Arity3] = DeriveSchema.gen[Arity3] + } final case class Arity24( a1: Arity1, a2: Arity2, @@ -360,12 +372,12 @@ object SchemaGen { f24: Int = 24 ) extends Arity + object Arity24 { + implicit val schema: Schema[Arity24] = DeriveSchema.gen[Arity24] + } + object Arity { - val arity1Schema: Schema[Arity1] = DeriveSchema.gen[Arity1] - val arity2Schema: Schema[Arity2] = DeriveSchema.gen[Arity2] - val arity3Schema: Schema[Arity3] = DeriveSchema.gen[Arity3] - val highAritySchema: Schema[Arity24] = DeriveSchema.gen[Arity24] - val arityEnumSchema: Schema[Arity] = DeriveSchema.gen[Arity] + implicit val arityEnumSchema: Schema[Arity] = DeriveSchema.gen[Arity] } val anyArity1: Gen[Random with Sized, Arity1] = Gen.anyInt.map(Arity1(_)) @@ -398,10 +410,10 @@ object SchemaGen { val anyCaseClassSchema: Gen[Random with Sized, Schema[_]] = Gen.oneOf( - Gen.const(Arity.arity1Schema), - Gen.const(Arity.arity2Schema), - Gen.const(Arity.arity3Schema), - Gen.const(Arity.highAritySchema) + Gen.const(Schema[Arity1]), + Gen.const(Schema[Arity2]), + Gen.const(Schema[Arity3]), + Gen.const(Schema[Arity24]) ) val anyCaseClassAndGen: Gen[Random with Sized, CaseClassAndGen[_]] =