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

diffson.jsonpointer.package.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

import cats._
import cats.implicits._
import cats.data.Chain

import io.estatico.newtype.macros.newtype

import scala.util.Try

import scala.language.{ implicitConversions, higherKinds }

package object jsonpointer {

  type Part = Either[String, Int]
  @newtype case class Pointer(parts: Chain[Part]) {

    def /(s: String): Pointer =
      Pointer(parts.append(Left(s)))

    def /(i: Int): Pointer =
      Pointer(parts.append(Right(i)))

    def evaluate[F[_], Json](json: Json)(implicit F: MonadError[F, Throwable], Json: Jsony[Json]): F[Json] =
      F.tailRecM((json, Pointer(parts), Pointer.Root)) {
        case (JsObject(obj), Inner(Left(elem), tl), parent) =>
          F.pure(Left((obj.getOrElse(elem, Json.Null), tl, parent / elem)))
        case (JsArray(arr), Inner(Right(idx), tl), parent) =>
          if (idx >= arr.size)
            // we know (by construction) that the index is greater or equal to zero
            F.raiseError(new PointerException(show"element $idx does not exist at path $parent"))
          else
            F.pure(Left(arr(idx), tl, parent / idx))
        case (value, Pointer.Root, _) =>
          F.pure(Right(value))
        case (_, Inner(elem, tl), parent) =>
          val elems = elem.fold(identity, _.toString)
          F.raiseError(new PointerException(show"element $elems does not exist at path $parent"))
      }

  }

  object Pointer {

    val Root: Pointer = Pointer(Chain.empty)

    private val IsNumber = "(0|[1-9][0-9]*)".r

    def apply(elems: String*): Pointer = Pointer(Chain.fromSeq(elems.map {
      case s @ IsNumber(idx) => Try(idx.toInt).liftTo[Either[Throwable, ?]].leftMap(_ => s)
      case key               => Left(key)
    }))

    def parse[F[_]](input: String)(implicit F: MonadError[F, Throwable]): F[Pointer] =
      if (input == null || input.isEmpty) {
        // shortcut if input is empty
        F.pure(Pointer.Root)
      } else if (!input.startsWith("/")) {
        // a pointer MUST start with a '/'
        F.raiseError(new PointerException("A JSON pointer must start with '/'"))
      } else {
        // first gets the different parts of the pointer
        val parts = input.split("/")
          // the first element is always empty as the path starts with a '/'
          .drop(1)
        if (parts.length == 0) {
          // the pointer was simply "/"
          F.pure(Pointer(""))
        } else {
          // check that an occurrence of '~' is followed by '0' or '1'
          if (parts.exists(_.matches(".*~(?![01]).*"))) {
            F.raiseError(new PointerException("Occurrences of '~' must be followed by '0' or '1'"))
          } else {
            val allParts = if (input.endsWith("/")) parts :+ "" else parts

            val elems = allParts
              // transform the occurrences of '~1' into occurrences of '/'
              // transform the occurrences of '~0' into occurrences of '~'
              .map(_.replace("~1", "/").replace("~0", "~"))
            F.pure(Pointer(elems: _*))
          }
        }
      }

    implicit val show: Show[Pointer] = Show.show[Pointer](pointer =>
      if (pointer.parts.isEmpty)
        ""
      else
        "/" + pointer.parts.map {
          case Left(l)  => l.replace("~", "~0").replace("/", "~1")
          case Right(r) => r.toString
        }.toList.mkString("/"))

  }

  object Inner {

    def unapply(parts: Pointer): Option[(Part, Pointer)] =
      parts.parts.uncons.map { case (h, t) => (h, Pointer(t)) }

  }

  object Leaf {

    def unapply(p: Pointer): Option[Part] =
      p.parts.uncons.flatMap {
        case (a, rest) if rest.isEmpty => Some(a)
        case _                         => None
      }

  }

  object ArrayIndex {
    def unapply(e: Part): Option[Int] = e.toOption
  }

  object ObjectField {
    def unapply(e: Part): Option[String] = Some(e.fold(identity, _.toString))
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy