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

18
import cats._
19
import cats.implicits._
20
import cats.data.Chain
21

22
import io.estatico.newtype.macros.newtype
23

24
import scala.util.Try
25

26
import scala.language.{ implicitConversions, higherKinds }
27
import scala.collection.compat._
28
import scala.collection.compat.immutable.ArraySeq
29

30
package object jsonpointer {
31

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

35
    def /(s: String): Pointer =
36 1
      Pointer(parts.append(Left(s)))
37

38
    def /(i: Int): Pointer =
39 1
      Pointer(parts.append(Right(i)))
40

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

58
  }
59

60
  object Pointer {
61

62 1
    val Root: Pointer = Pointer(Chain.empty)
63

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

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

71
    def parse[F[_]](input: String)(implicit F: MonadError[F, Throwable]): F[Pointer] =
72 1
      if (input == null || input.isEmpty) {
73
        // shortcut if input is empty
74 1
        F.pure(Pointer.Root)
75 1
      } else if (!input.startsWith("/")) {
76
        // a pointer MUST start with a '/'
77 1
        F.raiseError(new PointerException("A JSON pointer must start with '/'"))
78 1
      } else {
79
        // first gets the different parts of the pointer
80
        val parts = input.split("/")
81
          // the first element is always empty as the path starts with a '/'
82 1
          .drop(1)
83 1
        if (parts.length == 0) {
84
          // the pointer was simply "/"
85 1
          F.pure(Pointer(""))
86
        } else {
87
          // check that an occurrence of '~' is followed by '0' or '1'
88 1
          if (parts.exists(_.matches(".*~(?![01]).*"))) {
89 1
            F.raiseError(new PointerException("Occurrences of '~' must be followed by '0' or '1'"))
90 1
          } else {
91 1
            val allParts = if (input.endsWith("/")) parts :+ "" else parts
92

93
            val elems = allParts
94
              // transform the occurrences of '~1' into occurrences of '/'
95
              // transform the occurrences of '~0' into occurrences of '~'
96 1
              .map(_.replace("~1", "/").replace("~0", "~"))
97 1
            F.pure(Pointer(ArraySeq.unsafeWrapArray(elems): _*))
98
          }
99
        }
100
      }
101

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

111
  }
112

113
  object Inner {
114

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

118
  }
119

120
  object Leaf {
121

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

128
  }
129

130
  object ArrayIndex {
131 1
    def unapply(e: Part): Option[Int] = e.toOption
132
  }
133

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

138
}

Read our documentation on viewing source code .

Loading