All Downloads are FREE. Search and download functionalities are using the official Maven repository.

diffson.jsonpatch.JsonDiff.scala Maven / Gradle / Ivy

/*
* This file is part of the diffson project.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package diffson
package jsonpatch

import lcs._
import jsonpointer._

import cats.implicits._

import scala.annotation.tailrec

class JsonDiff[Json](diffArray: Boolean, rememberOld: Boolean)(implicit J: Jsony[Json], Lcs: Lcs[Json]) extends Diff[Json, JsonPatch[Json]] {
  def diff(json1: Json, json2: Json): JsonPatch[Json] =
    JsonPatch(diff(json1, json2, Pointer.Root))

  private def diff(json1: Json, json2: Json, pointer: Pointer): List[Operation[Json]] =
    if (json1 === json2)
      // if they are equal, this one is easy...
      Nil
    else (json1, json2) match {
      case (JsObject(fields1), JsObject(fields2))      => fieldsDiff(fields1.toList, fields2.toList, pointer)
      case (JsArray(arr1), JsArray(arr2)) if diffArray => arraysDiff(arr1.toList, arr2.toList, pointer)
      case (_, _)                                      => List(Replace(pointer, json2, if (rememberOld) Some(json1) else None))
    }

  private def fieldsDiff(fields1: List[(String, Json)], fields2: List[(String, Json)], path: Pointer): List[Operation[Json]] = {
    // sort fields by name in both objects
    val sorted1 = fields1.sortBy(_._1)
    val sorted2 = fields2.sortBy(_._1)
    @tailrec
    def associate(
      fields1: List[(String, Json)],
      fields2: List[(String, Json)],
      acc: List[(Option[(String, Json)], Option[(String, Json)])]): List[(Option[(String, Json)], Option[(String, Json)])] = (fields1, fields2) match {
      case (f1 :: t1, f2 :: t2) if f1._1 == f2._1 =>
        // same name, associate both
        associate(t1, t2, (Some(f1), Some(f2)) :: acc)
      case (f1 :: t1, f2 :: _) if f1._1 < f2._1 =>
        // the first field is not present in the second object
        associate(t1, fields2, (Some(f1), None) :: acc)
      case (f1 :: _, f2 :: t2) =>
        // the second field is not present in the first object
        associate(fields1, t2, (None, Some(f2)) :: acc)
      case (_, Nil) =>
        fields1.map(Some(_) -> None) ::: acc
      case (Nil, _) =>
        fields2.map(None -> Some(_)) ::: acc
    }
    @tailrec
    def fields(fs: List[(Option[(String, Json)], Option[(String, Json)])], acc: List[Operation[Json]]): List[Operation[Json]] = fs match {
      case (Some(f1), Some(f2)) :: tl if f1 == f2 =>
        // allright, nothing changed
        fields(tl, acc)
      case (Some(f1), Some(f2)) :: tl =>
        // same field name, different values
        fields(tl, diff(f1._2, f2._2, path / f1._1) ::: acc)
      case (Some(f1), None) :: tl =>
        // the field was deleted
        fields(tl, Remove[Json](path / f1._1, if (rememberOld) Some(f1._2) else None) :: acc)
      case (None, Some(f2)) :: tl =>
        // the field was added
        fields(tl, Add(path / f2._1, f2._2) :: acc)
      case _ =>
        acc
    }
    fields(associate(sorted1, sorted2, Nil), Nil)
  }

  private def arraysDiff(arr1: List[Json], arr2: List[Json], path: Pointer): List[Operation[Json]] = {
    // get the longest common subsequence in the array
    val lcs = Lcs.lcs(arr1, arr2)

    // indicates whether the index is in the lcs of the first sequence
    def isCommon1(idx1: Int, lcs: List[(Int, Int)]): Boolean = lcs match {
      case (cidx1, _) :: _ if idx1 == cidx1 => true
      case _                                => false
    }

    // indicates whether the index is in the lcs of the second sequence
    def isCommon2(idx2: Int, lcs: List[(Int, Int)]): Boolean = lcs match {
      case (_, cidx2) :: _ if idx2 == cidx2 => true
      case _                                => false
    }

    // add a bunch of values to an array starting at the specified index
    @tailrec
    def add(arr: List[Json], idx: Int, acc: List[Operation[Json]]): List[Operation[Json]] = arr match {
      case v :: tl => add(tl, idx + 1, Add(path / idx, v) :: acc)
      case Nil     => acc.reverse
    }

    // remove a bunch of array elements starting by the last one in the range
    def remove(from: Int, until: Int, shift: Int, arr: List[Json]): List[Operation[Json]] =
      (for (idx <- until to from by -1)
        yield Remove[Json](path / idx, if (rememberOld) Some(arr(idx - shift)) else None)).toList

    // now iterate over the first array to computes what was added, what was removed and what was modified
    @tailrec
    def loop(
      arr1: List[Json], // the first array
      arr2: List[Json], // the second array
      idx1: Int, // current index in the first array
      shift1: Int, // current index shift in the first array (due to elements being add or removed)
      idx2: Int, // current index in the second array
      lcs: List[(Int, Int)], // the list of remaining matching indices
      acc: List[Operation[Json]] // the already accumulated result
    ): List[Operation[Json]] = (arr1, arr2) match {
      case (_ :: tl1, _) if isCommon1(idx1, lcs) =>
        // all values in arr2 were added until the index of common value
        val until = lcs.head._2
        loop(tl1, arr2.drop(until - idx2 + 1), idx1 + 1, shift1 + until - idx2, until + 1, lcs.tail,
          add(arr2.take(until - idx2), idx1 + shift1, Nil) reverse_::: acc)
      case (_, _ :: tl2) if isCommon2(idx2, lcs) =>
        // all values in arr1 were removed until the index of common value
        val until = lcs.head._1
        loop(arr1.drop(until - idx1 + 1), tl2, until + 1, shift1 - (until - idx1), idx2 + 1, lcs.tail,
          remove(idx1 + shift1, until - 1 + shift1, idx1 + shift1, arr1) reverse_::: acc)
      case (v1 :: tl1, v2 :: tl2) =>
        // values are different, recursively compute the diff of these values
        loop(tl1, tl2, idx1 + 1, shift1, idx2 + 1, lcs, diff(v1, v2, path / (idx1 + shift1)) reverse_::: acc)
      case (_, Nil) =>
        // all subsequent values in arr1 were removed
        remove(idx1 + shift1, idx1 + arr1.size - 1 + shift1, idx1 + shift1, arr1) reverse_::: acc
      case (Nil, _) =>
        // all subsequent value in arr2 were added
        arr2.map(Add(path / "-", _)) reverse_::: acc
    }

    loop(arr1, arr2, 0, 0, 0, lcs, Nil).reverse
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy