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

sjc.delta.circe.json.scala Maven / Gradle / Ivy

The newest version!
package sjc.delta.circe

import io.circe.{Printer => PrettyParams, Encoder => EncodeJson, Json, JsonObject}
import io.circe.Json.{fromString => jString}
import sjc.delta.Delta.Aux
import sjc.delta.std.list.patience.{Removed, Equal, Inserted, Replaced}
import sjc.delta.{Patch, Delta}

import scala.reflect.ClassTag


object json extends json("left", "right", false) {
  object beforeAfter    extends json("before", "after", false)
  object actualExpected extends json("actual", "expected", false)
}

case class json(lhsName: String, rhsName: String, rfc6901Escaping: Boolean) { json =>
  object flat extends JsonDelta {
    def delta(left: Json, right: Json): Json = Json.obj(
      changes(left, right).map { case (pointer, change) => pointer.asString -> flatten(change) }: _*
    )
  }

  object compressed extends JsonDelta {
    def delta(left: Json, right: Json): Json = changes(left, right).foldLeft(jEmptyObject) {
      case (acc, (Pointer(path), change)) => add(acc, path, flatten(change))
    }

    private def add(json: Json, path: List[String], value: Json): Json = path match { // TODO: make tail recursive
      case Nil          => json
      case last :: Nil  => json.mapObject(o => o add (last, value))
      case head :: tail => json.mapObject(o => o add (head, add(o.apply(head).getOrElse(jEmptyObject), tail, value)))
    }
  }

  object rfc6902 extends JsonDelta {
    def delta(left: Json, right: Json): Json = Json.arr(changes(left, right) map { // TODO: Add 'move' & 'copy'
      case (pointer, Add(rightJ))        => op(pointer, "add",     Json.obj("value" -> rightJ))
      case (pointer, Remove(leftJ))      => op(pointer, "remove",  jEmptyObject)
      case (pointer, Replace(_, rightJ)) => op(pointer, "replace", Json.obj("value" -> rightJ))
    }: _*)

    private def op(pointer: Pointer, op: String, json: Json): Json =
      json.mapObject((obj: JsonObject) => ("op" -> jString(op)) +: ("path" -> pointer.jString) +: obj)
  }

  private def changes(leftJ: Json, rightJ: Json)(implicit deltaJ: Aux[Json, Json]): List[(Pointer, Change)] = {
    def recurse(pointer: Pointer, left: Option[Json], right: Option[Json]): List[(Pointer, Change)] = {
      if (left == right) Nil else (left, right) match {
        case (Some(JObject(leftO: JsonObject)), Some(JObject(rightO))) => {
          (leftO.keys.toSet ++ rightO.keys.toSet).toList.flatMap(field => {
            recurse(pointer + field, leftO.apply(field), rightO.apply(field))
          })
        }
        case (Some(JArray(leftA)), Some(JArray(rightA)))               => {
          sjc.delta.std.list.patience.deltaList[Json].apply(leftA, rightA) flatMap {
            case Removed(subSeq, removed) => subSeq.leftRange.zip(removed) flatMap {
              case (index, item) => (pointer + index).change(Some(item), None)
            }
            case Inserted(subSeq, inserted) => subSeq.rightRange.zip(inserted) flatMap {
              case (index, item) => (pointer + index).change(None, Some(item))
            }
            case Replaced(subSeq, removed, inserted) => subSeq.leftRange.zip(removed.zip(inserted)) flatMap {
              case (index, (rem, ins)) => recurse(pointer + index, Some(rem), Some(ins))
            }
            case Equal(_, _) => Nil
          }
        }
        case _ => pointer.change(left, right)
      }
    }

    recurse(Pointer(Nil), Some(leftJ), Some(rightJ))
  }

  private def flatten(change: Change): Json = change match {
    case Add(right)           => Json.fromJsonObject(missing(lhsName)  +: (rhsName -> right) +: JsonObject.empty)
    case Remove(left)         => Json.fromJsonObject((lhsName -> left)  +: missing(rhsName)  +: JsonObject.empty)
    case Replace(left, right) => Json.fromJsonObject((lhsName -> left)  +: (rhsName -> right) +: JsonObject.empty)
  }

  private def missing(name: String): (String, Json) = s"$name-missing" -> Json.True

  private sealed trait Change
  private case class Add(rightJ: Json)                  extends Change
  private case class Remove(leftJ: Json)                extends Change
  private case class Replace(leftJ: Json, rightJ: Json) extends Change

  private case class Pointer(elements: List[String]) { // http://tools.ietf.org/html/rfc6901
    def +(index: Int): Pointer = this + index.toString
    def +(element: String): Pointer = copy(element :: elements)

    def change(leftOJ: Option[Json], rightOJ: Option[Json]): List[(Pointer, Change)] = (leftOJ, rightOJ) match {
      case (None,                None) => Nil
      case (Some(leftJ),         None) => List(reverse -> Remove(leftJ))
      case (None,        Some(rightJ)) => List(reverse -> Add(rightJ))
      case (Some(leftJ), Some(rightJ)) => List(reverse -> Replace(leftJ, rightJ))
    }

    def jString: Json = Json.fromString(asString)
    def asString: String = if (elements.isEmpty) "" else "/" + elements.map(escape).mkString("/")

    private def escape(element: String): String = if (!rfc6901Escaping && element.startsWith("/")) s"[$element]" else {
      element.replace("~", "~0").replace("/", "~1")
    }

    private def reverse: Pointer = copy(elements.reverse)
  }

  private val jEmptyObject: Json = Json.fromJsonObject(JsonObject.empty)

  private object JObject { def unapply(json: Json): Option[JsonObject] = json.asObject   }
  private object JArray  { def unapply(json: Json): Option[List[Json]] = json.asArray.map(_.toList) }
}

trait JsonDelta {
  implicit def encodeJsonToDelta[A: EncodeJson]: Delta.Aux[A, Json] = jsonDelta.contramap[A](EncodeJson[A].apply)

  implicit val jsonDelta: Delta.Aux[Json, Json] with Patch[Json, String] = new Delta[Json] with Patch[Json, String] {
    type Out = Json

    def apply(left: Json, right: Json): Out = delta(left, right)
    def isEmpty(json: Json): Boolean = json == jEmptyObject
    def ignore(json: Json, paths: String*): Json = json.mapObject(obj => paths.foldLeft(obj)(_ remove _))
    def pretty(json: Json): String = PrettyParams.spaces2.print(json)

    protected val classTag: ClassTag[Json] = implicitly[ClassTag[Json]]
  }

  def delta(left: Json, right: Json): Json

  private val jEmptyObject: Json = Json.fromJsonObject(JsonObject.empty)
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy