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

commonMain.io.github.optimumcode.json.pointer.JsonPointer.kt Maven / Gradle / Ivy

There is a newer version: 0.3.0
Show newest version
package io.github.optimumcode.json.pointer

import kotlinx.serialization.Serializable
import kotlin.jvm.JvmField
import kotlin.jvm.JvmStatic

/**
 * Function to create [JsonPointer].
 * It is a more convenient way to create a JSON pointer rather than using [JsonPointer.compile] function
 */
public fun JsonPointer(path: String): JsonPointer = JsonPointer.compile(path)

/**
 * Implementation of a JSON pointer described in the specification
 * [RFC6901](https://datatracker.ietf.org/doc/html/rfc6901).
 */
@Serializable(JsonPointerSerializer::class)
public sealed class JsonPointer(
  internal open val next: JsonPointer? = null,
) {
  private var asString: String? = null
  private var hash: Int = 0

  /**
   * Creates a new [JsonPointer] that points to an [index] in the array.
   *
   * Example:
   * ```kotlin
   * val pointer = JsonPointer("/test").atIndex(0) // "/test/0"
   * ```
   */
  public fun atIndex(index: Int): JsonPointer {
    require(index >= 0) { "negative index: $index" }
    return insertLast(
      SegmentPointer(
        propertyName = index.toString(),
        depth = 1,
        index = index,
      ),
    )
  }

  /**
   * Creates a new [JsonPointer] that points to a [property] passed as a parameter.
   *
   * Example:
   * ```kotlin
   * val pointer = JsonPointer.ROOT.atProperty("prop1").atProperty("prop2")  // "/prop1/prop2"
   * ```
   */
  public fun atProperty(property: String): JsonPointer =
    insertLast(
      SegmentPointer(depth = 1, propertyName = property),
    )

  override fun toString(): String {
    val str = asString
    if (str != null) {
      return str
    }
    if (this !is SegmentPointer) {
      return ""
    }
    return buildString {
      var node: JsonPointer = this@JsonPointer
      while (node is SegmentPointer) {
        append(SEPARATOR)
        append(escapeJsonPointer(node.propertyName))
        node = node.next
      }
    }.also {
      asString = it
    }
  }

  internal fun insertLast(last: SegmentPointer): JsonPointer {
    if (this !is SegmentPointer) {
      return last
    }
    if (depth < MAX_POINTER_DEPTH_FOR_RECURSIVE_INSERT) {
      return insertLastDeepCopy(this, last)
    }
    // avoid recursion when pointer depth is greater than a specified limit
    // this should help with avoiding stack-overflow error
    // when this method called for a pointer that has too many segments
    //
    // Using queue is less efficient than recursion (around 10%) but saves us from crash
    val queue = ArrayDeque(depth)
    var cur: JsonPointer = this
    while (cur is SegmentPointer) {
      queue.add(cur)
      cur = cur.next
    }
    val additionalDepth = last.depth
    var result = last
    while (queue.isNotEmpty()) {
      val segment = queue.removeLast()
      result =
        SegmentPointer(
          propertyName = segment.propertyName,
          depth = segment.depth + additionalDepth,
          index = segment.index,
          next = result,
        )
    }
    return result
  }

  // there might be an issue with stack in case this function is called deep on the stack
  private fun insertLastDeepCopy(
    pointer: SegmentPointer,
    last: SegmentPointer,
  ): JsonPointer =
    with(pointer) {
      val additionalDepth = last.depth
      if (next is SegmentPointer) {
        SegmentPointer(
          propertyName = propertyName,
          depth = depth + additionalDepth,
          index = index,
          next = insertLastDeepCopy(next, last),
        )
      } else {
        SegmentPointer(
          propertyName = propertyName,
          depth = depth + additionalDepth,
          index = index,
          next = last,
        )
      }
    }

  private fun escapeJsonPointer(propertyName: String): String {
    if (propertyName.contains(SEPARATOR) || propertyName.contains(QUOTATION)) {
      return buildString(capacity = propertyName.length + 1) {
        for (ch in propertyName) {
          when (ch) {
            SEPARATOR -> append(QUOTATION).append(SEPARATOR_ESCAPE)
            QUOTATION -> append(QUOTATION).append(QUOTATION_ESCAPE)
            else -> append(ch)
          }
        }
      }
    }
    return propertyName
  }

  override fun equals(other: Any?): Boolean {
    if (this === other) return true
    if (other == null || this::class != other::class) return false

    other as JsonPointer

    var node = this
    var otherNode = other
    while (node is SegmentPointer && otherNode is SegmentPointer) {
      if (node.propertyName != otherNode.propertyName) {
        return false
      }
      node = node.next
      otherNode = otherNode.next
    }
    return node is EmptyPointer && otherNode is EmptyPointer
  }

  override fun hashCode(): Int {
    if (hash != 0) {
      return hash
    }
    var result = 31
    var node = this
    while (node is SegmentPointer) {
      result = 31 * result + node.propertyName.hashCode()
      node = node.next
    }
    if (result == 0) {
      // just in case if for some reason the resulting hash is zero
      // this way we won't recalculate it again
      result = 31
    }
    hash = result
    return result
  }

  public companion object {
    private const val MAX_POINTER_DEPTH_FOR_RECURSIVE_INSERT = 20
    internal const val SEPARATOR: Char = '/'
    internal const val QUOTATION: Char = '~'
    internal const val QUOTATION_ESCAPE: Char = '0'
    internal const val SEPARATOR_ESCAPE: Char = '1'

    /**
     * An empty [JsonPointer]. The empty JSON pointer corresponds to the current JSON element.s
     */
    @JvmField
    public val ROOT: JsonPointer = EmptyPointer

    private const val DEFAULT_BUFFER_CAPACITY = 32

    /**
     * Returns instance of the [JsonPointer] class.
     * If the [expr] is an empty string the [JsonPointer.ROOT] will be returned.
     *
     * If the [expr] is not an empty string it must start from the `/` character.
     *
     * @throws IllegalArgumentException the [expr] does not start from `/`
     */
    @JvmStatic
    public fun compile(expr: String): JsonPointer {
      return if (expr.isEmpty()) {
        EmptyPointer
      } else {
        require(expr.startsWith(SEPARATOR)) {
          "JSON pointer must start from $SEPARATOR: '$expr'"
        }
        parseExpression(expr)
      }
    }

    private class PointerParent(
      val parent: PointerParent?,
      val segment: String,
    )

    private fun buildPath(
      lastSegment: SegmentPointer,
      parent: PointerParent?,
    ): JsonPointer {
      var depth = lastSegment.depth
      var curr = lastSegment
      var parentValue = parent
      while (parentValue != null) {
        curr =
          parentValue.run {
            SegmentPointer(
              segment,
              ++depth,
              curr,
            )
          }
        parentValue = parentValue.parent
      }
      return curr
    }

    @JvmStatic
    private fun parseExpression(expr: String): JsonPointer {
      var parent: PointerParent? = null

      var offset = 1 // skip contextual slash
      val end = expr.length
      var start = 0
      while (offset < end) {
        val currentChar = expr[offset]
        if (currentChar == SEPARATOR) {
          parent = PointerParent(parent, expr.substring(start + 1, offset))
          start = offset
          offset++
          continue
        }
        offset++
        if (currentChar == QUOTATION && offset < end) {
          val builder = StringBuilder(DEFAULT_BUFFER_CAPACITY)
          offset = builder.appendEscapedSegment(expr, start + 1, offset)
          val segment = builder.toString()
          if (offset < 0) {
            return buildPath(SegmentPointer(segment), parent)
          }
          parent = PointerParent(parent, segment)
          start = offset
          offset++
          continue
        }
      }
      return buildPath(SegmentPointer(expr.substring(start + 1)), parent)
    }
  }
}

private fun StringBuilder.appendEscapedSegment(
  expr: String,
  start: Int,
  offset: Int,
): Int {
  var pos: Int = offset
  val end = expr.length
  val needCopy = pos - 1 - start > 0
  if (needCopy) {
    append(expr, start, pos - 1)
  }
  appendEscaped(expr[pos])
  pos++
  while (pos < end) {
    val currentChar = expr[pos]
    if (currentChar == JsonPointer.SEPARATOR) {
      return pos
    }
    pos++
    if (currentChar == '~' && pos < end) {
      appendEscaped(expr[pos])
      pos++
      continue
    }
    append(currentChar)
  }
  return -1
}

private fun StringBuilder.appendEscaped(ch: Char) {
  val result =
    when (ch) {
      JsonPointer.QUOTATION_ESCAPE -> JsonPointer.QUOTATION
      JsonPointer.SEPARATOR_ESCAPE -> JsonPointer.SEPARATOR
      else -> {
        append(JsonPointer.QUOTATION)
        ch
      }
    }

  append(result)
}

internal object EmptyPointer : JsonPointer()

internal class SegmentPointer(
  val propertyName: String,
  val depth: Int = 1,
  override val next: JsonPointer = EmptyPointer,
  val index: Int = parseIndex(propertyName),
) : JsonPointer(next) {
  companion object {
    private const val NO_INDEX: Int = -1
    private const val LONG_LENGTH_THRESHOLD = 10

    @JvmStatic
    private fun parseIndex(segment: String): Int {
      if (segment.isEmpty()) {
        return NO_INDEX
      }
      val len = segment.length
      // super long indexes are no good
      // let's assume we don't have any array over 2 billion entries
      if (len > LONG_LENGTH_THRESHOLD) {
        return NO_INDEX
      }
      return parseIndexValue(segment)
    }

    private fun parseIndexValue(segment: String): Int {
      val len = segment.length
      val ch = segment[0]
      if (ch <= '0') {
        return if (len == 1 && ch == '0') {
          0
        } else {
          NO_INDEX
        }
      }
      // not a number
      if (ch > '9') {
        return NO_INDEX
      }

      for (char in segment) {
        if (char !in '0'..'9') {
          return NO_INDEX
        }
      }

      return if (len == LONG_LENGTH_THRESHOLD) {
        // check the index fits integer
        val index = segment.toLong()
        if (index > Int.MAX_VALUE) {
          NO_INDEX
        } else {
          index.toInt()
        }
      } else {
        segment.toInt()
      }
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy