forked from zio/zio-schema
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fixes zio#78: Implement diffing between instances described by a schema
- Loading branch information
1 parent
3aaa434
commit cd6531f
Showing
4 changed files
with
224 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,209 @@ | ||
package zio.schema | ||
|
||
import java.math.BigInteger | ||
import java.time.{ DayOfWeek, Instant } | ||
import java.util.concurrent.TimeUnit | ||
|
||
import scala.concurrent.duration.TimeUnit | ||
|
||
import zio.Chunk | ||
|
||
trait DiffAlgorithm[A] { self => | ||
|
||
def apply(thisValue: A, thatValue: A): Diff | ||
|
||
/** | ||
* A symbolic operator for [[zip]]. | ||
*/ | ||
def <*>[B](that: DiffAlgorithm[B]): DiffAlgorithm[(A, B)] = self.zip(that) | ||
|
||
def zip[B](that: DiffAlgorithm[B]): DiffAlgorithm[(A, B)] = DiffAlgorithm.tuple(self, that) | ||
|
||
def transform[B](f: B => A): DiffAlgorithm[B] = DiffAlgorithm.instance { (thisValue: B, thatValue: B) => | ||
self.apply(f(thisValue), f(thatValue)) | ||
} | ||
|
||
def transformOrFail[B](f: B => Either[String, A]): DiffAlgorithm[B] = DiffAlgorithm.instance { | ||
(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]): DiffAlgorithm[Col[A]] = DiffAlgorithm.instance { | ||
(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 | ||
} | ||
) | ||
} | ||
|
||
def optional: DiffAlgorithm[Option[A]] = DiffAlgorithm.instance { | ||
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 DiffAlgorithm { | ||
|
||
def fromSchema[A](schema: Schema[A]): DiffAlgorithm[A] = schema match { | ||
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.BigDecimalType) => bigDecimal | ||
case Schema.Primitive(StandardType.BigIntegerType) => bigInt | ||
case Schema.Primitive(StandardType.Duration(_)) => | ||
temporal[java.time.Duration](TimeUnit.MILLISECONDS)( | ||
(d1: java.time.Duration, d2: java.time.Duration) => d1.minus(d2).toMillis | ||
) | ||
case Schema.Primitive(StandardType.DayOfWeekType) => dayOfWeek | ||
case Schema.Primitive(StandardType.Instant(_)) => | ||
temporal[Instant](TimeUnit.MILLISECONDS)((i1, i2) => i1.toEpochMilli - i2.toEpochMilli) | ||
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 _ => string.transform(_.toString) | ||
} | ||
|
||
def numeric[A](implicit numeric: Numeric[A]) = | ||
instance { (thisValue: A, thatValue: A) => | ||
numeric.minus(thisValue, thatValue) match { | ||
case distance if distance == numeric.zero => Diff.Identical | ||
case distance => Diff.Number(distance) | ||
} | ||
} | ||
|
||
def temporal[A](units: TimeUnit)(metric: (A, A) => Long) = | ||
instance { (thisValue: A, thatValue: A) => | ||
Diff.Temporal(metric(thisValue, thatValue), units) | ||
} | ||
|
||
val dayOfWeek = | ||
instance { (thisValue: DayOfWeek, thatValue: DayOfWeek) => | ||
var distance = 0L | ||
do { | ||
distance += 1 | ||
} while (!thisValue.plus(distance).equals(thatValue)) | ||
Diff.Temporal(distance, TimeUnit.DAYS) | ||
} | ||
|
||
val bigInt: DiffAlgorithm[BigInteger] = | ||
instance { (thisValue: BigInteger, thatValue: BigInteger) => | ||
thisValue.subtract(thatValue) match { | ||
case BigInteger.ZERO => Diff.Identical | ||
case distance => Diff.BigInt(distance) | ||
} | ||
} | ||
|
||
val bigDecimal: DiffAlgorithm[java.math.BigDecimal] = | ||
instance { (thisValue: java.math.BigDecimal, thatValue: java.math.BigDecimal) => | ||
thisValue.subtract(thatValue) match { | ||
case java.math.BigDecimal.ZERO => Diff.Identical | ||
case distance => Diff.BigDecimal(distance) | ||
} | ||
} | ||
|
||
def tuple[A, B](left: DiffAlgorithm[A], right: DiffAlgorithm[B]) = | ||
instance { (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: DiffAlgorithm[A], right: DiffAlgorithm[B]) = | ||
instancePartial[Either[A, B]] { | ||
case (Left(l), Left(r)) => left(l, r) | ||
case (Right(l), Right(r)) => right(l, r) | ||
} | ||
|
||
def fail[A]: DiffAlgorithm[A] = | ||
instance((_: A, _: A) => Diff.NotComparable) | ||
|
||
/** | ||
* Port implementation of Myers diff algorithm from zio-test here | ||
*/ | ||
val string: DiffAlgorithm[String] = ??? | ||
|
||
def instance[A](f: (A, A) => Diff): DiffAlgorithm[A] = | ||
new DiffAlgorithm[A] { | ||
override def apply(thisValue: A, thatValue: A): Diff = f(thisValue, thatValue) | ||
} | ||
|
||
def instancePartial[A](f: PartialFunction[(A, A), Diff]) = | ||
new DiffAlgorithm[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 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: TimeUnit) extends Diff | ||
|
||
final case class Tuple(leftDifference: Diff, rightDifference: Diff) 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 | ||
|
||
/** | ||
* Set of elements which differ between two sets. | ||
*/ | ||
final case class Set[A](differences: Set[(A, Tag, Diff)]) extends Diff | ||
|
||
sealed trait Tag | ||
|
||
object Tag { | ||
case object Left extends Tag | ||
case object Right extends Tag | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
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) | ||
} | ||
} |