Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# CHANGELOG

# Release 3.1.0-SNAPSHOT
* TODO
* Add: structural equality check for `NormalizedJsonPath` and `NormalizedJsonPathSegment`

# Release 3.0.1
* Fix: Validates whether the whole string has been consumed
Expand Down
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ repositories {
google()
gradlePluginPortal()
}

nexusPublishing {
nexusPublishing {
repositories {
Expand Down
1 change: 1 addition & 0 deletions jsonpath4k/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ kotlin {
}
commonTest {
dependencies {
implementation(kotlin("test"))
implementation(libs.kotest.common)
implementation(libs.kotest.property)
implementation(libs.kotest.assertions.core)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,54 +1,83 @@
package at.asitplus.jsonpath.core

import kotlinx.serialization.Serializable
import kotlin.jvm.JvmInline

/**
* specification: https://datatracker.ietf.org/doc/rfc9535/
* date: 2024-02
* section: 2.7. Normalized Paths
*/
@Serializable
class NormalizedJsonPath(
@JvmInline
value class NormalizedJsonPath(
@Deprecated("Access to this variable will be removed in the future, please use the path itself as list.")
val segments: List<NormalizedJsonPathSegment> = listOf(),
) {
) : List<NormalizedJsonPathSegment> by segments {
constructor(vararg segments: NormalizedJsonPathSegment) : this(segments = segments.asList())

operator fun plus(other: NormalizedJsonPath): NormalizedJsonPath {
return NormalizedJsonPath(this.segments + other.segments)
@ExperimentalUnsignedTypes
constructor(vararg segments: UInt) : this(
segments.map {
NormalizedJsonPathSegment.IndexSegment(it)
}
)

constructor(vararg segments: String) : this(
segments.map {
NormalizedJsonPathSegment.NameSegment(it)
}
)

companion object {
operator fun invoke(vararg segments: Int) = NormalizedJsonPath(segments.map {
require(it >= 0) {
"Expected index segments to be non-negative, but got $it"
}
NormalizedJsonPathSegment.IndexSegment(it)
})
}

operator fun plus(segment: NormalizedJsonPathSegment) = this + NormalizedJsonPath(segment)
operator fun plus(
other: List<NormalizedJsonPathSegment>
) = NormalizedJsonPath(this.segments + other)

operator fun plus(segment: NormalizedJsonPathSegment) = this + listOf(segment)

operator fun plus(memberName: String) = this + NormalizedJsonPathSegment.NameSegment(memberName)

operator fun plus(index: UInt) = this + NormalizedJsonPathSegment.IndexSegment(index)

override fun toString(): String {
return "$${segments.joinToString("")}"
return "$${joinToString("")}"
}

fun toNormalizedJsonPathString() = toString()

@Throws(Throwable::class)
fun toShorthandNameSegmentNotation(): String {
return "$${segments.joinToString("") {
when(it) {
is NormalizedJsonPathSegment.IndexSegment -> it.toString()
is NormalizedJsonPathSegment.NameSegment -> it.toShorthandNotation()
return "$${
joinToString("") {
when (it) {
is NormalizedJsonPathSegment.IndexSegment -> it.toString()
is NormalizedJsonPathSegment.NameSegment -> it.toShorthandNotation()
}
}
}}"
}"
}

fun toShorthandNameSegmentNotationWherePossible(): String {
return "$${segments.joinToString("") {
when(it) {
is NormalizedJsonPathSegment.IndexSegment -> it.toNormalizedJsonPathSegmentString()
is NormalizedJsonPathSegment.NameSegment -> try {
it.toShorthandNotation()
} catch (_: Throwable) {
it.toNormalizedJsonPathSegmentString()
return "$${
joinToString("") {
when (it) {
is NormalizedJsonPathSegment.IndexSegment -> it.toNormalizedJsonPathSegmentString()
is NormalizedJsonPathSegment.NameSegment -> try {
it.toShorthandNotation()
} catch (_: Throwable) {
it.toNormalizedJsonPathSegmentString()
}
}
}
}}"
}"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,32 @@ package at.asitplus.jsonpath.core

import at.asitplus.jsonpath.generated.JsonPathLexer
import at.asitplus.jsonpath.generated.JsonPathParser
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long
import org.antlr.v4.kotlinruntime.CharStreams
import org.antlr.v4.kotlinruntime.CommonTokenStream
import org.antlr.v4.kotlinruntime.ListTokenSource
import kotlin.jvm.JvmInline

/**
* specification: https://datatracker.ietf.org/doc/rfc9535/
* date: 2024-02
* section: 2.7. Normalized Paths
*/
@Serializable
@Serializable(with = NormalizedJsonPathSegment.JsonDistinguishableSerializer::class)
sealed interface NormalizedJsonPathSegment {
fun toNormalizedJsonPathSegmentString(): String

@Serializable
class NameSegment(val memberName: String) : NormalizedJsonPathSegment {
@JvmInline
value class NameSegment(val memberName: String) : NormalizedJsonPathSegment {
override fun toNormalizedJsonPathSegmentString() = toString()

override fun toString(): String {
Expand Down Expand Up @@ -48,11 +58,52 @@ sealed interface NormalizedJsonPathSegment {
}

@Serializable
class IndexSegment(val index: UInt) : NormalizedJsonPathSegment {
@JvmInline
value class IndexSegment(val index: UInt) : NormalizedJsonPathSegment {
companion object {
operator fun invoke(int: Int) = IndexSegment(int.also {
require(int >= 0)
}.toUInt())
}

override fun toNormalizedJsonPathSegmentString() = toString()

override fun toString(): String {
return "[$index]"
}
}

class JsonDistinguishableSerializer : KSerializer<NormalizedJsonPathSegment> {
override val descriptor: SerialDescriptor
get() = SerialDescriptor(
original = JsonElement.serializer().descriptor,
serialName = JsonDistinguishableSerializer::class.qualifiedName!!,
)

override fun serialize(
encoder: Encoder,
value: NormalizedJsonPathSegment
) {
when (value) {
is IndexSegment -> encoder.encodeLong(value.index.toLong())
is NameSegment -> encoder.encodeString(value.memberName)
}
}

override fun deserialize(decoder: Decoder): NormalizedJsonPathSegment {
require(decoder is JsonDecoder) {
"Expected decoder to be ${JsonDecoder::class.qualifiedName!!}, but was `$decoder`."
}
val jsonElement = decoder.decodeJsonElement().jsonPrimitive
return if (jsonElement.isString) {
NameSegment(jsonElement.content)
} else {
IndexSegment(jsonElement.long.also {
require(it >= 0) {
"Expected index segment to be non-negative, but got `$it`."
}
}.toUInt())
}
}
}
}
Comment thread
nodh marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package at.asitplus.jsonpath.core

import io.kotest.matchers.maps.shouldHaveSize
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNot
import io.kotest.matchers.shouldNotBe
import kotlin.test.Test

@Suppress("unused")
class NormalizedJsonPathTest {
@Test
fun equality() {
NormalizedJsonPath() shouldBe NormalizedJsonPath()
NormalizedJsonPath("name") shouldBe NormalizedJsonPath() + "name"
NormalizedJsonPath(1) shouldBe NormalizedJsonPath() + 1u

mapOf(
NormalizedJsonPath() to "1",
NormalizedJsonPath() to "2",
) shouldHaveSize 1
}
@Test
fun inequality() {
NormalizedJsonPath(1) shouldNotBe NormalizedJsonPath()
NormalizedJsonPath(1) shouldNotBe NormalizedJsonPath() + "1"
NormalizedJsonPath(1) shouldNotBe NormalizedJsonPath() + 2
NormalizedJsonPath("1") shouldNotBe NormalizedJsonPath()
NormalizedJsonPath("1") shouldNotBe NormalizedJsonPath() + "2"
NormalizedJsonPath("1") shouldNotBe NormalizedJsonPath() + 1
NormalizedJsonPath() shouldNotBe NormalizedJsonPath() + "1"
NormalizedJsonPath() shouldNotBe NormalizedJsonPath() + 1
}
}
Loading