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

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

The newest version!
/*
* 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 jsonpointer._

import cats._
import cats.implicits._

import scala.annotation.tailrec
import scala.language.higherKinds

/** A patch operation to apply to a Json value */
sealed abstract class Operation[Json: Jsony] {
  val path: Pointer

  def apply[F[_]](value: Json)(implicit F: MonadError[F, Throwable]): F[Json] =
    action[F](value, path, Pointer.Root)

  // the action to perform in this operation. By default returns an object that is equal
  protected[this] def action[F[_]](value: Json, pointer: Pointer, parent: Pointer)(implicit F: MonadError[F, Throwable]): F[Json] = (value, pointer) match {
    case (_, Pointer.Root) =>
      F.pure(value)
    case (JsObject(fields), Inner(ObjectField(elem), tl)) if fields.contains(elem) =>
      action[F](fields(elem), tl, parent / elem)
        .map(fields.updated(elem, _))
        .map(JsObject(_))
    case (JsArray(elems), Inner(ArrayIndex(idx), tl)) =>
      if (idx >= elems.size) {
        F.raiseError(new PatchException(show"element $idx does not exist at path $parent"))
      } else {
        action[F](elems(idx), tl, parent / idx)
          .map { updated =>
            val (before, after) = elems.splitAt(idx)
            JsArray(before ++ (updated +: after.tail))
          }
      }
    case (_, Inner(elem, _)) =>
      F.raiseError(new PatchException(show"element ${elem.fold(identity[String], _.toString)} does not exist at path $parent"))
  }

}

/** Add (or replace if existing) the pointed element */
case class Add[Json: Jsony](path: Pointer, value: Json) extends Operation[Json] {

  override protected[this] def action[F[_]](original: Json, pointer: Pointer, parent: Pointer)(implicit F: MonadError[F, Throwable]): F[Json] =
    (original, pointer) match {
      case (_, Pointer.Root) =>
        // we are at the root value, simply return the replacement value
        F.pure(value)
      case (JsArray(arr), Leaf(Left("-"))) =>
        // insert the value at the end of the array
        F.pure(JsArray(arr :+ value))
      case (JsArray(arr), Leaf(ArrayIndex(idx))) =>
        if (idx > arr.size) {
          F.raiseError(new PatchException(show"element $idx does not exist at path $parent"))
        } else {
          // insert the value at the specified index
          val (before, after) = arr.splitAt(idx)
          F.pure(JsArray(before ++ (value +: after)))
        }
      case (JsObject(obj), Leaf(ObjectField(lbl))) =>
        // insert the new label
        F.pure(JsObject(obj.updated(lbl, value)))
      case _ =>
        super.action[F](original, pointer, parent)
    }

}

/** Remove the pointed element */
case class Remove[Json: Jsony](path: Pointer, old: Option[Json] = None) extends Operation[Json] {

  override protected[this] def action[F[_]](value: Json, pointer: Pointer, parent: Pointer)(implicit F: MonadError[F, Throwable]): F[Json] =
    (value, pointer) match {
      case (JsArray(arr), Leaf(ArrayIndex(idx))) =>
        if (idx >= arr.size) {
          // we know thanks to the extractor that the index cannot be negative
          F.raiseError(new PatchException(show"element $idx does not exist at path $parent"))
        } else {
          // remove the element at the given index
          val (before, after) = arr.splitAt(idx)
          F.pure(JsArray(before ++ after.tail))
        }
      case (JsArray(_), Leaf(Left("-"))) =>
        // how could we possibly remove an element that appears after the last one?
        F.raiseError(new PatchException(show"element - does not exist at path $parent"))
      case (JsObject(obj), Leaf(ObjectField(lbl))) if obj.contains(lbl) =>
        // remove the field from the object if present, otherwise, ignore it
        F.pure(JsObject(obj - lbl))
      case (_, Pointer.Root) =>
        F.raiseError(new PatchException("Cannot delete an empty path"))
      case _ =>
        super.action[F](value, pointer, parent)
    }

}

/** Replace the pointed element by the given value */
case class Replace[Json: Jsony](path: Pointer, value: Json, old: Option[Json] = None) extends Operation[Json] {

  override protected[this] def action[F[_]](original: Json, pointer: Pointer, parent: Pointer)(implicit F: MonadError[F, Throwable]): F[Json] =
    (original, pointer) match {
      case (_, Pointer.Root) =>
        // simply replace the root value by the replacement value
        F.pure(value)
      case (JsArray(arr), Leaf(Right(idx))) =>
        if (idx >= arr.size)
          F.raiseError(new PatchException(show"element $idx does not exist at path $parent"))
        else
          F.pure(JsArray(arr.updated(idx, value)))
      case (JsArray(_), Leaf(Left("-"))) =>
        F.raiseError(new PatchException(show"element - does not exist at path $parent"))
      case (JsObject(obj), Leaf(ObjectField(lbl))) =>
        if (obj.contains(lbl))
          F.pure(JsObject(obj.updated(lbl, value)))
        else
          F.raiseError(new PatchException(show"element $lbl does not exist at path $parent"))
      case _ =>
        super.action[F](original, pointer, parent)
    }

}

/** Move the pointed element to the new position */
case class Move[Json: Jsony](from: Pointer, path: Pointer) extends Operation[Json] {

  override def apply[F[_]](original: Json)(implicit F: MonadError[F, Throwable]): F[Json] = {
    @tailrec
    def prefix(p1: Pointer, p2: Pointer): Boolean = (p1, p2) match {
      case (Inner(h1, tl1), Inner(h2, tl2)) if h1 == h2 => prefix(tl1, tl2)
      case (Pointer.Root, Inner(_, _))                  => true
      case (_, _)                                       => false
    }
    if (prefix(from, path))
      F.raiseError(new PatchException("The destination path cannot be a descendant of the source path"))
    else
      for {
        value <- from.evaluate[F, Json](original)
        cleaned <- Remove[Json](from).apply[F](original)
        res <- Add[Json](path, value).apply[F](cleaned)
      } yield res
  }

}

/** Copy the pointed element to the new position */
case class Copy[Json: Jsony](from: Pointer, path: Pointer) extends Operation[Json] {

  override def apply[F[_]](original: Json)(implicit F: MonadError[F, Throwable]): F[Json] = for {
    value <- from.evaluate[F, Json](original)
    res <- Add[Json](path, value).apply[F](original)
  } yield res

}

/** Test that the pointed element is equal to the given value */
case class Test[Json: Jsony](path: Pointer, value: Json) extends Operation[Json] {

  override def apply[F[_]](original: Json)(implicit F: MonadError[F, Throwable]): F[Json] =
    path.evaluate[F, Json](original).flatMap { orig =>
      if (value != orig)
        F.raiseError(new PatchException(show"test failed at path $path"))
      else
        F.pure(original)
    }

}

case class JsonPatch[Json: Jsony](ops: List[Operation[Json]]) {
  def apply[F[_]](json: Json)(implicit F: MonadError[F, Throwable]) =
    ops.foldM(json)((json, op) => op[F](json))
}

object JsonPatch {

  implicit def JsonPatchPatch[F[_], Json](implicit F: MonadError[F, Throwable], Json: Jsony[Json]): Patch[F, Json, JsonPatch[Json]] =
    new Patch[F, Json, JsonPatch[Json]] {
      def apply(json: Json, patch: JsonPatch[Json]): F[Json] =
        patch[F](json)
    }

  def apply[Json: Jsony](ops: Operation[Json]*): JsonPatch[Json] =
    JsonPatch(ops.toList)

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy