From f90972958f07adad1223907495a2adddabe1f684 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Wed, 22 Jul 2026 17:58:48 +0200 Subject: [PATCH 01/10] new antlr, more targets, more tests, conventions --- README.md | 2 +- build.gradle.kts | 28 +- gradle.properties | 11 +- gradle/libs.versions.toml | 33 +- gradle/wrapper/gradle-wrapper.jar | Bin 63375 -> 45633 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 28 +- gradlew.bat | 25 +- jsonpath4k/build.gradle.kts | 295 ++++--- jsonpath4k/javadoc/README.txt | 1 + .../src/commonTest/kotlin/KotestConfig.kt | 9 - .../jsonpath/DependencyManagementTest.kt | 44 +- .../at/asitplus/jsonpath/JsonPathUnitTest.kt | 733 ++++++------------ .../MemberNameShorthandSerializationTest.kt | 35 +- .../jsonpath/NodeListSerializationTest.kt | 11 +- .../NormalizedJsonPathSerializationTest.kt | 49 +- .../asitplus/jsonpath/Rfc9535UtilsUnitTest.kt | 257 ++---- .../jsonpath/core/NormalizedJsonPathTest.kt | 14 +- .../jsonpath/AntlrConcurrencyNativeTest.kt | 198 +++++ settings.gradle.kts | 10 +- 20 files changed, 794 insertions(+), 991 deletions(-) create mode 100644 jsonpath4k/javadoc/README.txt delete mode 100644 jsonpath4k/src/commonTest/kotlin/KotestConfig.kt create mode 100644 jsonpath4k/src/nativeTest/kotlin/at/asitplus/jsonpath/AntlrConcurrencyNativeTest.kt diff --git a/README.md b/README.md index c81bdd7..37385e9 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![A-SIT Plus Official](https://raw.githubusercontent.com/a-sit-plus/a-sit-plus.github.io/709e802b3e00cb57916cbb254ca5e1a5756ad2a8/A-SIT%20Plus_%20official_opt.svg)](https://plus.a-sit.at/open-source.html) [![GitHub license](https://img.shields.io/badge/license-Apache%20License%202.0-brightgreen.svg?style=flat)](http://www.apache.org/licenses/LICENSE-2.0) [![Kotlin](https://img.shields.io/badge/kotlin-multiplatform-orange.svg?logo=kotlin)](http://kotlinlang.org) -[![Kotlin](https://img.shields.io/badge/kotlin-2.0.20-blue.svg?logo=kotlin)](http://kotlinlang.org) +[![Kotlin](https://img.shields.io/badge/kotlin-2.4.0-blue.svg?logo=kotlin)](http://kotlinlang.org) [![Java](https://img.shields.io/badge/java-17+-blue.svg?logo=OPENJDK)](https://www.oracle.com/java/technologies/downloads/#java17) [![Maven Central](https://img.shields.io/maven-central/v/at.asitplus/jsonpath4k)](https://mvnrepository.com/artifact/at.asitplus/jsonpath4k/) diff --git a/build.gradle.kts b/build.gradle.kts index 9f264f0..1c256d2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,28 +1,10 @@ plugins { - // this is necessary to avoid the plugins to be loaded multiple times - // in each subproject's classloader - alias(libs.plugins.gradle.nexus.publish) - alias(libs.plugins.android.library) apply (false) -} - -repositories { - maven("https://maven.pkg.jetbrains.space/kotlin/p/dokka/dev") - mavenCentral() - google() - gradlePluginPortal() -} - -nexusPublishing { - nexusPublishing { - repositories { - sonatype { - nexusUrl.set(uri("https://ossrh-staging-api.central.sonatype.com/service/local/")) - snapshotRepositoryUrl.set(uri("https://central.sonatype.com/repository/maven-snapshots/")) - } - } - } + kotlin("multiplatform") version libs.versions.kotlin.get() apply false + kotlin("plugin.serialization") version libs.versions.kotlin.get() apply false + alias(libs.plugins.android.kmp.library) apply false + alias(libs.plugins.asp.conventions) } val artifactVersion: String by extra group = "at.asitplus" -version = artifactVersion \ No newline at end of file +version = artifactVersion diff --git a/gradle.properties b/gradle.properties index 1ae26fd..ee30cc0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,8 @@ #Gradle org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M" +# Tests use TestBalloon + the matrix addon (wired in by the asp-conventions plugin). +# Keep the legacy Kotest helper disabled; TestBalloon supplies the Kotest assertion/property integration. +systemProp.KOTEST_NO_ASP_HELPER=true #Kotlin kotlin.code.style=official @@ -13,11 +16,7 @@ kotlin.mpp.stability.nowarn=true kotlin.native.ignoreDisabledTargets=true -# workaround dokka bug (need to wait for next snapshot build) -org.jetbrains.dokka.classpath.excludePlatformDependencyFiles=true - artifactVersion = 3.2.0-SNAPSHOT jdk.version=17 - -android.experimental.lint.version=8.5.0 -android.lint.useK2Uast=true +android.minSdk=30 +android.compileSdk=34 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7f64890..772c0ba 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,33 +1,16 @@ [versions] -antlr-kotlin = "1.0.3" +kotlin = "2.4.0" +asp-conventions = "20260701" +agp = "8.12.3" +antlr-kotlin = "1.0.12" kotest = "5.9.1" -kotest-plugin = "6.0.0.M1" -jetbrains-kotlin = "2.1.0" -jetbrains-kotlinx-serialization = "1.8.0" -jetbrains-dokka = "1.9.20" -napier = "2.7.1" -gradle-nexus-publish = "2.0.0" -agp = "8.2.2" +testballoon = "1.0.1-K2.4.0" [libraries] antlr-kotlin = { group = "com.strumenta", name = "antlr-kotlin-runtime", version.ref = "antlr-kotlin" } -napier = { module = "io.github.aakira:napier", version.ref = "napier" } - -kotest-common = { module = "io.kotest:kotest-common", version.ref = "kotest" } -kotest-property = { module = "io.kotest:kotest-property", version.ref = "kotest" } -kotest-assertions-core = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" } -kotest-framework-engine = { module = "io.kotest:kotest-framework-engine", version.ref = "kotest" } -kotest-framework-datatest = { module = "io.kotest:kotest-framework-datatest", version.ref = "kotest" } -kotest-runner-junit5 = { module = "io.kotest:kotest-runner-junit5", version.ref = "kotest" } - -jetbrains-kotlinx-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "jetbrains-kotlinx-serialization" } - [plugins] +asp-conventions = { id = "at.asitplus.gradle.conventions", version.ref = "asp-conventions" } +android-kmp-library = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" } antlr-kotlin-plugin = { id = "com.strumenta.antlr-kotlin", version.ref = "antlr-kotlin" } -kotest-multiplatform = { id = "io.kotest.multiplatform", version.ref = "kotest-plugin" } -jetbrains-kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "jetbrains-kotlin" } -jetbrains-kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "jetbrains-kotlin" } -jetbrains-dokka = { id = "org.jetbrains.dokka", version.ref = "jetbrains-dokka" } -gradle-nexus-publish = { id = "io.github.gradle-nexus.publish-plugin", version.ref = "gradle-nexus-publish" } -android-library = {id = "com.android.library", version.ref = "agp"} \ No newline at end of file +testballoon = { id = "de.infix.testBalloon", version.ref = "testballoon" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 033e24c4cdf41af1ab109bc7f253b2b887023340..f8e1ee3125fe0768e9a76ee977ac089eb657005e 100644 GIT binary patch literal 45633 zcma&NV|1n6wyqu9PQ|uu+csuwn-$x(T~Woh?Nr6KUD3(A)@l1Yd+oj6Z_U=8`RAE` z#vE6_`?!1WLs1443=Ieh3JM4ai0JG2|2{}S&_HrxszP*9^5P7#QX*pVDq?D?;6T8C z{bWO1$9at%!*8ax*TT&F99vwf1Ls+3lklsb|bC`H`~Q z_w}*E9P=Wq;PYlGYhZ^lt#N97bt5aZ#mQcOr~h^B;R>f-b0gf{y(;VA{noAt`RZzU z7vQWD{%|q!urW2j0Z&%ChtL(^9m` zgaU%|B;V#N_?%iPvu0PVkX=1m9=*SEGt-Lp#&Jh%rz6EJXlV^O5B5YfM5j{PCeElx z8sipzw8d=wVhFK+@mgrWyA)Sv3BJq=+q+cL@=wuH$2;LjY z^{&+X4*HFA0{QvlM_V4PTQjIdd;d|2YuN;s|bi!@<)r-G%TuOCHz$O(_-K z)5in&6uNN<0UfwY=K>d;cL{{WK2FR|NihJMN0Q4X+(1lE)$kY?T$7UWleIU`i zQG#X-&&m-8x^(;n@o}$@vPMYRoq~|FqC~CU3MnoiifD{(CwAGd%X#kFHq#4~%_a!{ zeX{XXDT#(DvX7NtAs7S}2ZuiZ>gtd;tCR7E)3{J^`~#Vd**9qz%~JRFAiZf{zt|Dr zvQw!)n7fNUn_gH`o9?8W8t_%x6~=y*`r46bjj(t{YU*qfqd}J}*mkgUfsXTI>Uxl6 z)Fj>#RMy{`wINIR;{_-!xGLgVaTfNJ2-)%YUfO&X5z&3^E#4?k-_|Yv$`fpgYkvnA%E{CiV zP|-zAf8+1@R`sT{rSE#)-nuU7Pwr-z>0_+CLQT|3vc-R22ExKT4ym@Gj77j$aTVns zp4Kri#Ml?t7*n(;>nkxKdhOU9Qbwz%*#i9_%K<`m4T{3aPbQ?J(Mo`6E5cDdbAk%X z+4bN%E#a(&ZXe{G#V!2Nt+^L$msKVHP z|APpBhq7knz(O2yY)$$VyI_Xg4UIC*$!i7qQG~KEZnO@Q1i89@4ZKW*3^Wh?o?zSkfPxdhnTxlO!3tAqe_ zuEqHVcAk3uQIFTpP~C{d$?>7yt3G3Fo>syXTus>o0tJdFpQWC27hDiwC%O09i|xCq z@H6l|+maB;%CYQIChyhu;PVYz9e&5a@EEQs3$DS6dLIS+;N@I0)V}%B`jdYv;JDck zd|xxp(I?aedivE7*19hesoa-@Xm$^EHbbVmh$2^W-&aTejsyc$i+}A#n2W*&0Qt`5 zJS!2A|LVV;L!(*x2N)GjJC;b1RB_f(#D&g_-};a*|BTRvfdIX}Gau<;uCylMNC;UG zzL((>6KQBQ01wr%7u9qI2HLEDY!>XisIKb#6=F?pAz)!_JX}w|>1V>X^QkMdFi@Jr z`1N*V4xUl{qvECHoF?#lXuO#Dg2#gh|AU$Wc=nuIbmVPBEGd(R#&Z`TP9*o%?%#ob zWN%ByU+55yBNfjMjkJnBjT!cVDi}+PR3N&H(f8$d^Pu;A_WV*{)c2Q{IiE7&LPsd4 z!rvkUf{sco_WNSIdW+btM#O+4n`JiceH6%`7pDV zRqJ@lj=Dt(e-Gkz$b!c2>b)H$lf(fuAPdIsLSe(dZ4E~9+Ge!{3j~>nS%r)eQZ;Iq ztWGpp=2Ptc!LK_TQ8cgJXUlU5mRu|7F2{eu*;a>_5S<;bus=t*IXcfzJRPv4xIs;s zt2<&}OM>KxkTxa=dFMfNr42=DL~I}6+_{`HT_YJBiWkpVZND1Diad~Yr*Fuq{zljr z*_+jXk=qVBdwlQkYuIrB4GG*#voba$?h*u0uRNL+87-?AjzG2X_R9mzQ7BJEawutObr|ey~%in>6k%A`K*`pb-|DF5m})!`b=~osoiW2)IFh?_y9y<3Cix_ znvC=bjBX1J820!%%9FaB@v?hAsd05e@w$^ZAvtUp*=Bi+Owkl?rLa6F#yl{s+?563 zmn2 zV95%gySAJ$L!Vvk4kx!n@mo`3Mfi`2lXUkBmd%)u)7C?Pa;oK~zUQ#p0u{a|&0;zNO#9a4`v^3df90X#~l_k$q7n&L5 z?TszF842~g+}tgUP}UG?ObLCE1(Js_$e>XS7m%o7j@@VdxePtg)w{i5an+xK95r?s zDeEhgMO-2$H?@0{p-!4NJ)}zP+3LzZB?FVap)ObHV6wp}Lrxvz$cjBND1T6ln$EfJ zZRPeR2lP}K0p8x`ahxB??Ud;i7$Y5X!5}qBFS+Zp=P^#)08nQi_HuJcN$0=x;2s53 zwoH}He9BlKT4GdWfWt)@o@$4zN$B@5gVIN~aHtwIhh{O$uHiMgYl=&Vd$w#B2 zRv+xK3>4E{!)+LXA2#*K6H~HpovXAQeXV(^Pd%G_>ro0(4_@`{2Ag(+8{9pqJ>Co$ zRRV(oX;nD+Jel_2^BlNO=cQP8q*G#~R3PTERUxvug_C4T3qwb9MQE|^{5(H*nt`fn z^%*p-RwkAhT6(r>E@5w8FaB)Q<{#`H9fTdc6QBuSr9D-x!Tb9f?wI=M{^$cB5@1;0 z+yLHh?3^c-Qte@JI<SW`$bs5Vv9!yWjJD%oY z8Cdc$a(LLy@tB2)+rUCt&0$&+;&?f~W6+3Xk3g zy9L�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 literal 63375 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≫L+crb^Bz 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 /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -111,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -144,7 +146,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +154,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -169,7 +171,6 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -201,16 +202,15 @@ fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 93e3f59..c4bdd3a 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,22 +59,21 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/jsonpath4k/build.gradle.kts b/jsonpath4k/build.gradle.kts index 4208555..657a3b9 100644 --- a/jsonpath4k/build.gradle.kts +++ b/jsonpath4k/build.gradle.kts @@ -1,36 +1,27 @@ +import at.asitplus.gradle.Logger +import at.asitplus.gradle.coroutines +import at.asitplus.gradle.exportXCFramework +import at.asitplus.gradle.napier +import at.asitplus.gradle.serialization +import at.asitplus.gradle.setupDokka import com.strumenta.antlrkotlin.gradle.AntlrKotlinTask -import org.jetbrains.dokka.gradle.DokkaTask -import org.jetbrains.dokka.gradle.DokkaTaskPartial -import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension -import org.jetbrains.kotlin.gradle.dsl.kotlinExtension -import org.jetbrains.kotlin.gradle.plugin.mpp.BitcodeEmbeddingMode -import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget -import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFrameworkConfig -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl +import org.jetbrains.kotlin.gradle.plugin.extraProperties +import java.util.Properties +import kotlin.apply plugins { - alias(libs.plugins.android.library) - alias(libs.plugins.jetbrains.kotlin.multiplatform) - alias(libs.plugins.jetbrains.kotlinx.serialization) - alias(libs.plugins.kotest.multiplatform) + alias(libs.plugins.android.kmp.library) + kotlin("multiplatform") + kotlin("plugin.serialization") alias(libs.plugins.antlr.kotlin.plugin) - alias(libs.plugins.jetbrains.dokka) - id("maven-publish") + alias(libs.plugins.testballoon) id("signing") + alias(libs.plugins.asp.conventions) } -/* required for maven publication */ -val artifactVersion: String by extra -group = "at.asitplus" -version = artifactVersion - -repositories { - google() - maven("https://s01.oss.sonatype.org/content/repositories/snapshots") //KOTEST snapshot - mavenCentral() -} - +group = rootProject.group +version = rootProject.version val SRCDIR_ANTRL = "generated/antlr" val generateKotlinGrammarSource = tasks.register("generateKotlinGrammarSource") { @@ -53,97 +44,101 @@ val generateKotlinGrammarSource = tasks.register("generateKotli outputDirectory = layout.buildDirectory.dir(outDir).get().asFile } +val Project.disableNdkTargets + get() = ("true" == (System.getenv("disableNdkTargets") + ?.also { Logger.lifecycle(" > Property disableNdkTargets set to $it from environment") } + ?: runCatching { + (project.extraProperties["disableNdkTargets"] as String).also { + Logger.lifecycle(" > Property disableNdkTargets set to $it from extra properties") + } + }.getOrNull())) + kotlin { - jvmToolchain(17) - - jvm { - compilations.all { - kotlinOptions { - jvmTarget = "17" - freeCompilerArgs = listOf( - "-Xjsr305=strict" - ) - } - } + androidLibrary { + namespace = "at.asitplus.jsonpath4k" } - - androidTarget { - compilerOptions { - publishLibraryVariants("release") - jvmTarget = JvmTarget.JVM_1_8 + jvm() + macosArm64() + tvosArm64() + tvosSimulatorArm64() + iosArm64() + iosSimulatorArm64() + watchosSimulatorArm64() + watchosArm32() + watchosArm64() + tvosSimulatorArm64() + tvosArm64() + + if (project.hasAndroidSdk()) { + if (project.hasAndroidNdk() && !project.disableNdkTargets) { + androidNativeX64() + androidNativeX86() + androidNativeArm32() + androidNativeArm64() + } else { + Logger.lifecycle(" > Skipping Android native targets (NDK missing or disableNdkTargets=true)") } } + listOf( + js().apply { browser { testTask { enabled = false } } }, + @OptIn(ExperimentalWasmDsl::class) + wasmJs().apply { browser { testTask { enabled = false } } }, + // wasmWasi() + ).forEach { + it.nodejs() + } - iosArm64() - iosSimulatorArm64() - iosX64() - + linuxX64() + linuxArm64() + mingwX64() sourceSets { commonMain { kotlin.srcDir(layout.buildDirectory.dir(SRCDIR_ANTRL)) dependencies { implementation(libs.antlr.kotlin) - implementation(libs.jetbrains.kotlinx.serialization) - implementation(libs.napier) + implementation(serialization("json")) + implementation(napier()) } } commonTest { dependencies { - implementation(kotlin("test")) - implementation(libs.kotest.common) - implementation(libs.kotest.property) - implementation(libs.kotest.assertions.core) - implementation(libs.kotest.framework.engine) - implementation(libs.kotest.framework.datatest) - implementation(libs.jetbrains.kotlinx.serialization) - } - } - - jvmTest { - dependencies { - implementation(libs.kotest.runner.junit5) + // TestBalloon, the matrix addon, Kotest assertions and Kotest property are wired in + // automatically by the asp-conventions plugin (unless TESTBALLOON_NO_ASP_HELPER is set). + implementation(serialization("json")) + // Needed by the highly-concurrent native ANTLR stress test in nativeTest. + implementation(coroutines()) } } } } -tasks.withType> { - dependsOn(generateKotlinGrammarSource) -} -tasks.withType { +tasks.withType>().configureEach { dependsOn(generateKotlinGrammarSource) } tasks.withType { dependsOn(generateKotlinGrammarSource) } +// Note: the JVM test task (JUnit Platform + launcher) is configured by the TestBalloon Gradle plugin, +// which the asp-conventions plugin applies; do not call useJUnitPlatform() manually. -exportIosFramework("JsonPath4K") +exportXCFramework("JsonPath4K", transitiveExports = false) val javadocJar = setupDokka( - baseUrl = "https://github.com/a-sit-plus/jsonpath4k/tree/main/", - multiModuleDoc = false + baseUrl = "https://github.com/a-sit-plus/jsonpath4k/tree/main" ) - -android { - namespace = "at.asitplus.jsonpath4k" - compileSdk = 34 - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - defaultConfig { - minSdk = 30 - } +val javadocRedirectJar = tasks.register("javadocRedirectJar") { + archiveClassifier.set("javadoc") + from(project.rootDir.absolutePath+"/javadoc") } publishing { publications { withType { - if (this.name != "relocation") artifact(javadocJar) + if (this.name != "relocation") artifact(javadocRedirectJar) pom { name.set("JsonPath4K") description.set("Kotlin Multiplatform library for using Json Paths as specified in [RFC9535](https://datatracker.ietf.org/doc/rfc9535/)") @@ -201,86 +196,88 @@ signing { } -/** - * taken from vclib conventions plugin at https://github.com/a-sit-plus/gradle-conventions-plugin - */ -fun Project.exportIosFramework( - name: String, - vararg additionalExports: Any -) = exportIosFramework(name, bitcodeEmbeddingMode = BitcodeEmbeddingMode.BITCODE, additionalExports = additionalExports) - -fun Project.exportIosFramework( - name: String, - bitcodeEmbeddingMode: BitcodeEmbeddingMode, - vararg additionalExports: Any -) { - val iosTargets = kotlinExtension.let { - if (it is KotlinMultiplatformExtension) { - it.targets.filterIsInstance().filter { it.name.startsWith("ios") } - } else throw StopExecutionException("No iOS Targets found! Declare them explicitly before calling exportIosFramework!") - } - extensions.getByType().apply { - XCFrameworkConfig(project, name).also { xcf -> - logger.lifecycle(" \u001B[1mXCFrameworks will be exported for the following iOS targets: ${iosTargets.joinToString { it.name }}\u001B[0m") - iosTargets.forEach { - it.binaries.framework { - baseName = name - embedBitcode(bitcodeEmbeddingMode) - additionalExports.forEach { export(it) } - xcf.add(this) - } + +fun Project.hasAndroidSdk() = resolveAndroidSdk(this)?.let { it -> isValidAndroidSdk(it) } == true + +fun Project.hasAndroidNdk() = resolveAndroidNdk(this)?.let { it -> isValidAndroidNdk(it) } == true + +private fun resolveAndroidSdk(project: Project): File? { + // Highest precedence: ANDROID_SDK_ROOT (preferred), then ANDROID_HOME (legacy) + val env = System.getenv() + val fromEnv = listOf("ANDROID_SDK_ROOT", "ANDROID_HOME") + .asSequence() + .mapNotNull { env[it]?.takeIf { it.isNotBlank() } } + .map(::File) + .firstOrNull { it.exists() } + + if (fromEnv != null) return fromEnv + + // Fallback: local.properties (common on dev machines) + val localProps = File(project.rootDir, "local.properties") + if (localProps.exists()) { + Properties().apply { + localProps.inputStream().use(::load) + (getProperty("sdk.dir") ?: getProperty("android.sdk.path"))?.let { + val f = File(it) + if (f.exists()) return f } } } + return null } -fun Project.setupDokka( - outputDir: String = rootProject.layout.buildDirectory.dir("dokka").get().asFile.canonicalPath, - baseUrl: String, - multiModuleDoc: Boolean = false, - remoteLineSuffix: String = "#L" -): TaskProvider { - val dokkaHtml = (tasks["dokkaHtml"] as DokkaTask).apply { outputDirectory.set(file(outputDir)) } - - val deleteDokkaOutput = tasks.register("deleteDokkaOutputDirectory") { - delete(outputDir) - } - val sourceLinktToConfigure = if (multiModuleDoc) (tasks["dokkaHtmlPartial"] as DokkaTaskPartial) else dokkaHtml - sourceLinktToConfigure.dokkaSourceSets.configureEach { - sourceLink { - localDirectory.set(file("src/$name/kotlin")) - remoteUrl.set(uri("$baseUrl/${project.name}/src/$name/kotlin").toURL()) - this@sourceLink.remoteLineSuffix.set(remoteLineSuffix) +private fun resolveAndroidNdk(project: Project): File? { + val env = System.getenv() + val fromEnv = listOf("ANDROID_NDK_ROOT", "ANDROID_NDK_HOME", "ANDROID_NDK", "NDK_HOME") + .asSequence() + .mapNotNull { env[it]?.takeIf { it.isNotBlank() } } + .map(::File) + .firstOrNull { it.exists() } + if (fromEnv != null) return fromEnv + + val localProps = File(project.rootDir, "local.properties") + if (localProps.exists()) { + Properties().apply { + localProps.inputStream().use(::load) + (getProperty("ndk.dir") ?: getProperty("ndkDirectory"))?.let { + val f = File(it) + if (f.exists()) return f + } } } - return tasks.register("javadocJar") { - dependsOn(deleteDokkaOutput, dokkaHtml) - archiveClassifier.set("javadoc") - from(outputDir) + val sdk = resolveAndroidSdk(project) ?: return null + val bundled = listOf(File(sdk, "ndk-bundle"), File(sdk, "ndk")) + .firstOrNull { it.exists() } + + // $SDK/ndk is a folder containing versioned subfolders; pick the "latest" directory. + if (bundled?.name == "ndk") { + val candidates = bundled.listFiles()?.filter { it.isDirectory } ?: emptyList() + return candidates.maxWithOrNull { a, b -> + val ta = a.name.toVersionTuple() + val tb = b.name.toVersionTuple() + val n = maxOf(ta.size, tb.size) + (0 until n).asSequence() + .map { i -> (ta.getOrNull(i) ?: 0).compareTo(tb.getOrNull(i) ?: 0) } + .firstOrNull { it != 0 } ?: 0 + } ?: bundled } + + return bundled } +private fun String.toVersionTuple(): List = + split('.', '-', '_').mapNotNull { it.toIntOrNull() }.ifEmpty { listOf(0) } -afterEvaluate { - tasks.withType { - useJUnitPlatform() - } +private fun isValidAndroidNdk(ndk: File): Boolean { + val prebuilt = File(ndk, "toolchains/llvm/prebuilt") + return prebuilt.isDirectory && (prebuilt.listFiles()?.any { it.isDirectory } == true) +} - /** - * Makes all publishing tasks depend on all signing tasks. Hampers parallelization, but works around dodgy task dependencies - * which (more often than anticipated) makes the build process stumble over its own feet. - */ - - tasks.withType().also { signingTasks -> - if (signingTasks.isNotEmpty()) { - logger.lifecycle("> Making signing tasks of project \u001B[1m$name\u001B[0m run after publish tasks") - tasks.withType().configureEach { - mustRunAfter(*signingTasks.toTypedArray()) - logger.lifecycle(" * $name must now run after ${signingTasks.joinToString { it.name }}") - } - logger.lifecycle("") - } - } -} \ No newline at end of file + +private fun isValidAndroidSdk(sdk: File): Boolean { + val platformsOk = File(sdk, "platforms").listFiles()?.any { it.isDirectory } == true + val buildToolsOk = File(sdk, "build-tools").listFiles()?.any { it.isDirectory } == true + return platformsOk && buildToolsOk +} diff --git a/jsonpath4k/javadoc/README.txt b/jsonpath4k/javadoc/README.txt new file mode 100644 index 0000000..598d1f2 --- /dev/null +++ b/jsonpath4k/javadoc/README.txt @@ -0,0 +1 @@ +https://a-sit-plus.github.io/jsonpath4k/ \ No newline at end of file diff --git a/jsonpath4k/src/commonTest/kotlin/KotestConfig.kt b/jsonpath4k/src/commonTest/kotlin/KotestConfig.kt deleted file mode 100644 index 91cf641..0000000 --- a/jsonpath4k/src/commonTest/kotlin/KotestConfig.kt +++ /dev/null @@ -1,9 +0,0 @@ -import io.github.aakira.napier.DebugAntilog -import io.github.aakira.napier.Napier -import io.kotest.core.config.AbstractProjectConfig - -object KotestConfig : AbstractProjectConfig() { - init { - Napier.base(DebugAntilog()) - } -} \ No newline at end of file diff --git a/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/DependencyManagementTest.kt b/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/DependencyManagementTest.kt index 6393894..dc84865 100644 --- a/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/DependencyManagementTest.kt +++ b/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/DependencyManagementTest.kt @@ -3,37 +3,41 @@ package at.asitplus.jsonpath import at.asitplus.jsonpath.core.JsonPathCompiler import at.asitplus.jsonpath.core.JsonPathFunctionExtension import at.asitplus.jsonpath.core.JsonPathQuery +import at.asitplus.testballoon.matrix.matrixConfig +import at.asitplus.testballoon.matrix.matrixSuite +import de.infix.testBalloon.framework.core.TestConfig +import de.infix.testBalloon.framework.core.aroundEachTest import io.kotest.assertions.throwables.shouldNotThrowAny -import io.kotest.core.spec.style.FreeSpec import io.kotest.matchers.collections.shouldBeIn import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.nulls.shouldNotBeNull import kotlinx.serialization.json.JsonNull import kotlinx.serialization.json.buildJsonObject -@Suppress("unused") -class DependencyManagementTest : FreeSpec({ - // making sure that the dependencies are reset to their default for the next test - val defaultCompilerBuilderBackup = JsonPath.defaultCompiler - val defaultFunctionExtensionRepositoryBackup = - JsonPath.defaultFunctionExtensionRepository.export() +// Captured once, before any test mutates the process-global JsonPath dependencies. +private val defaultCompilerBuilderBackup = JsonPath.defaultCompiler +private val defaultFunctionExtensionRepositoryBackup = + JsonPath.defaultFunctionExtensionRepository.export() - beforeEach { - // prepare a dummy repository to be modified by the tests - JsonPath.defaultFunctionExtensionRepository = - JsonPathFunctionExtensionMapRepository( - defaultFunctionExtensionRepositoryBackup.toMutableMap() - ) - } - afterEach { - JsonPath.Companion.apply { - defaultCompiler = defaultCompilerBuilderBackup - defaultFunctionExtensionRepository = JsonPathFunctionExtensionMapRepository( +val DependencyManagementTest by matrixSuite( + matrixConfig { + // Reset the global dependencies to their defaults around every test (was beforeEach/afterEach). + testConfig = TestConfig.aroundEachTest { action -> + // prepare a dummy repository to be modified by the tests + JsonPath.defaultFunctionExtensionRepository = JsonPathFunctionExtensionMapRepository( defaultFunctionExtensionRepositoryBackup.toMutableMap() ) + try { + action() + } finally { + JsonPath.defaultCompiler = defaultCompilerBuilderBackup + JsonPath.defaultFunctionExtensionRepository = JsonPathFunctionExtensionMapRepository( + defaultFunctionExtensionRepositoryBackup.toMutableMap() + ) + } } } - +) { "dependency manager compiler should support the functions in the repo at the time of compilation, and query should be executable afterwards too" - { "compiler that was built when the repository supported a function extension before it was removed should succeed compilation before and query afterwards" { val jsonPathStatement = "$[?foo()]" @@ -73,4 +77,4 @@ class DependencyManagementTest : FreeSpec({ // this checks, whether the new compiler has indeed been used emptyQueryResult.shouldHaveSize(0) } -}) +} diff --git a/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/JsonPathUnitTest.kt b/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/JsonPathUnitTest.kt index 198712c..296370c 100644 --- a/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/JsonPathUnitTest.kt +++ b/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/JsonPathUnitTest.kt @@ -4,9 +4,12 @@ import at.asitplus.jsonpath.core.JsonPathFilterExpressionType import at.asitplus.jsonpath.core.JsonPathFunctionExtension import at.asitplus.jsonpath.implementation.JsonPathParserException import at.asitplus.jsonpath.implementation.JsonPathTypeCheckerException +import at.asitplus.testballoon.matrix.MatrixSuiteScope +import at.asitplus.testballoon.matrix.matrixSuite +import de.infix.testBalloon.framework.core.TestConfig +import de.infix.testBalloon.framework.core.aroundEachTest import io.kotest.assertions.throwables.shouldNotThrowAny import io.kotest.assertions.throwables.shouldThrow -import io.kotest.core.spec.style.FreeSpec import io.kotest.matchers.collections.shouldBeIn import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.nulls.shouldNotBeNull @@ -20,8 +23,35 @@ import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive -@Suppress("unused") -class JsonPathUnitTest : FreeSpec({ +/** + * Registers a FreeSpec-style test whose name IS the JSONPath expression, compiles that exact expression, + * queries [json] and hands the resulting value list to [assertions]. Mirrors the original Kotest idiom that + * recovered the expression from `testCase.name.originalName`. + */ +private fun MatrixSuiteScope.query( + path: String, + json: JsonElement, + assertions: (nodeList: List) -> Unit, +) = path { assertions(JsonPath(path).query(json).map { it.value }) } + +/** Test named after [path] asserting that compiling it succeeds. */ +private fun MatrixSuiteScope.compiles(path: String) = + path { shouldNotThrowAny { JsonPath(path) } } + +/** Test named after [path] asserting that compiling it fails type checking. */ +private fun MatrixSuiteScope.failsTypeCheck(path: String) = + path { shouldThrow { JsonPath(path) } } + +/** Test named after [path] asserting that compiling it fails parsing. */ +private fun MatrixSuiteScope.failsParsing(path: String) = + path { shouldThrow { JsonPath(path) } } + +// Captured once, before any test in section 2.4 mutates the process-global JsonPath dependencies. +private val defaultCompilerBuilderBackup = JsonPath.defaultCompiler +private val defaultFunctionExtensionRepositoryBackup = + JsonPath.defaultFunctionExtensionRepository.export() + +val JsonPathUnitTest by matrixSuite { "Examples from https://datatracker.ietf.org/doc/rfc9535/" - { "1.5. JSONPath Examples" - { val bookStore = Json.decodeFromString( @@ -58,9 +88,7 @@ class JsonPathUnitTest : FreeSpec({ " }" ) - "$.store.book[*].author" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(bookStore).map { it.value } + query("$.store.book[*].author", bookStore) { nodeList -> nodeList shouldHaveSize 4 val bookArray = bookStore.jsonObject["store"].shouldNotBeNull().jsonObject["book"].shouldNotBeNull().jsonArray @@ -70,9 +98,7 @@ class JsonPathUnitTest : FreeSpec({ } } - "$..author" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(bookStore).map { it.value } + query("$..author", bookStore) { nodeList -> nodeList shouldHaveSize 4 val bookArray = bookStore.jsonObject["store"].shouldNotBeNull().jsonObject["book"].shouldNotBeNull().jsonArray @@ -82,9 +108,7 @@ class JsonPathUnitTest : FreeSpec({ } } - "\$.store.*" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(bookStore).map { it.value } + query("\$.store.*", bookStore) { nodeList -> nodeList shouldHaveSize 2 val store = bookStore.jsonObject["store"].shouldNotBeNull().jsonObject @@ -92,9 +116,7 @@ class JsonPathUnitTest : FreeSpec({ store.get("bicycle").shouldNotBeNull().shouldBeIn(nodeList) } - "\$.store..price" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(bookStore).map { it.value } + query("\$.store..price", bookStore) { nodeList -> nodeList shouldHaveSize 5 val store = bookStore.jsonObject["store"].shouldNotBeNull().jsonObject @@ -105,9 +127,7 @@ class JsonPathUnitTest : FreeSpec({ .shouldBeIn(nodeList) } - "\$..book[2]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(bookStore).map { it.value } + query("\$..book[2]", bookStore) { nodeList -> nodeList shouldHaveSize 1 bookStore.jsonObject["store"] .shouldNotBeNull().jsonObject["book"] @@ -115,9 +135,7 @@ class JsonPathUnitTest : FreeSpec({ .shouldNotBeNull().shouldBeIn(nodeList) } - "\$..book[2].author" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(bookStore).map { it.value } + query("\$..book[2].author", bookStore) { nodeList -> nodeList shouldHaveSize 1 bookStore.jsonObject["store"] .shouldNotBeNull().jsonObject["book"] @@ -126,15 +144,11 @@ class JsonPathUnitTest : FreeSpec({ .shouldNotBeNull().shouldBeIn(nodeList) } - "\$..book[2].publisher" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(bookStore).map { it.value } + query("\$..book[2].publisher", bookStore) { nodeList -> nodeList shouldHaveSize 0 } - "\$..book[-1]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(bookStore).map { it.value } + query("\$..book[-1]", bookStore) { nodeList -> nodeList shouldHaveSize 1 bookStore.jsonObject["store"] .shouldNotBeNull().jsonObject["book"] @@ -142,9 +156,7 @@ class JsonPathUnitTest : FreeSpec({ .shouldNotBeNull().shouldBeIn(nodeList) } - "\$..book[0,1]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(bookStore).map { it.value } + query("\$..book[0,1]", bookStore) { nodeList -> nodeList shouldHaveSize 2 bookStore.jsonObject["store"] .shouldNotBeNull().jsonObject["book"] @@ -155,9 +167,7 @@ class JsonPathUnitTest : FreeSpec({ } } - "\$..book[:2]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(bookStore).map { it.value } + query("\$..book[:2]", bookStore) { nodeList -> nodeList shouldHaveSize 2 bookStore.jsonObject["store"] .shouldNotBeNull().jsonObject["book"] @@ -168,9 +178,7 @@ class JsonPathUnitTest : FreeSpec({ } } - "\$..book[?@.isbn]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(bookStore).map { it.value } + query("\$..book[?@.isbn]", bookStore) { nodeList -> nodeList shouldHaveSize 2 bookStore.jsonObject["store"] .shouldNotBeNull().jsonObject["book"] @@ -181,9 +189,7 @@ class JsonPathUnitTest : FreeSpec({ } } - "\$..book[?@.price<10]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(bookStore).map { it.value } + query("\$..book[?@.price<10]", bookStore) { nodeList -> nodeList shouldHaveSize 2 bookStore.jsonObject["store"] .shouldNotBeNull().jsonObject["book"] @@ -194,9 +200,7 @@ class JsonPathUnitTest : FreeSpec({ } } - "\$..*" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(bookStore).map { it.value } + query("\$..*", bookStore) { nodeList -> bookStore.jsonObject["store"] .shouldNotBeNull().shouldBeIn(nodeList).jsonObject.let { store -> store["book"].shouldNotBeNull() @@ -216,29 +220,21 @@ class JsonPathUnitTest : FreeSpec({ "2.1. Overview" - { val jsonElement = Json.decodeFromString("{\"a\":[{\"b\":0},{\"b\":1},{\"c\":2}]}") - "\$" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$", jsonElement) { nodeList -> nodeList shouldHaveSize 1 jsonElement.shouldBeIn(nodeList) } - "\$.a" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$.a", jsonElement) { nodeList -> nodeList shouldHaveSize 1 jsonElement.jsonObject["a"].shouldNotBeNull().shouldBeIn(nodeList) } - "\$.a[*]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$.a[*]", jsonElement) { nodeList -> nodeList shouldHaveSize 3 jsonElement.jsonObject["a"].shouldNotBeNull().jsonArray.forEach { it.shouldBeIn(nodeList) } } - "\$.a[*].b" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$.a[*].b", jsonElement) { nodeList -> nodeList shouldHaveSize 2 jsonElement.jsonObject["a"].shouldNotBeNull().jsonArray.forEach { it.shouldBeInstanceOf()["b"]?.jsonPrimitive?.shouldBeIn(nodeList) @@ -248,9 +244,7 @@ class JsonPathUnitTest : FreeSpec({ "2.2. Root Identifier" - { val jsonElement = Json.decodeFromString("{\"k\": \"v\"}") - "$" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("$", jsonElement) { nodeList -> nodeList shouldHaveSize 1 jsonElement.shouldBeIn(nodeList) } @@ -263,35 +257,27 @@ class JsonPathUnitTest : FreeSpec({ " \"'\": {\"@\": 2}\n" + " }" ) - "\$.o['j j']" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$.o['j j']", jsonElement) { nodeList -> nodeList shouldHaveSize 1 jsonElement.jsonObject.get("o") .shouldNotBeNull().jsonObject["j j"] .shouldNotBeNull().shouldBeIn(nodeList) } - "\$.o['j j']['k.k']" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$.o['j j']['k.k']", jsonElement) { nodeList -> nodeList shouldHaveSize 1 jsonElement.jsonObject.get("o") .shouldNotBeNull().jsonObject["j j"] .shouldNotBeNull().jsonObject["k.k"] .shouldNotBeNull().shouldBeIn(nodeList) } - "\$.o[\"j j\"][\"k.k\"]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$.o[\"j j\"][\"k.k\"]", jsonElement) { nodeList -> nodeList shouldHaveSize 1 jsonElement.jsonObject.get("o") .shouldNotBeNull().jsonObject["j j"] .shouldNotBeNull().jsonObject["k.k"] .shouldNotBeNull().shouldBeIn(nodeList) } - "\$[\"'\"][\"@\"]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$[\"'\"][\"@\"]", jsonElement) { nodeList -> nodeList shouldHaveSize 1 jsonElement.jsonObject.get("'") .shouldNotBeNull().jsonObject["@"] @@ -306,18 +292,14 @@ class JsonPathUnitTest : FreeSpec({ " \"a\": [5, 3]\n" + " }" ) - "\$[*]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$[*]", jsonElement) { nodeList -> nodeList shouldHaveSize 2 jsonElement.jsonObject.get("o") .shouldNotBeNull().shouldBeIn(nodeList) jsonElement.jsonObject.get("a") .shouldNotBeNull().shouldBeIn(nodeList) } - "\$.o[*]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$.o[*]", jsonElement) { nodeList -> nodeList shouldHaveSize 2 jsonElement.jsonObject["o"].shouldNotBeNull().let { it.jsonObject["j"] @@ -326,9 +308,7 @@ class JsonPathUnitTest : FreeSpec({ .shouldNotBeNull().shouldBeIn(nodeList) } } - "\$.o[*, *]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$.o[*, *]", jsonElement) { nodeList -> nodeList shouldHaveSize 4 jsonElement.jsonObject["o"].shouldNotBeNull().let { it.jsonObject["j"] @@ -345,9 +325,7 @@ class JsonPathUnitTest : FreeSpec({ } } } - "\$.a[*]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$.a[*]", jsonElement) { nodeList -> nodeList shouldHaveSize 2 jsonElement.jsonObject["a"].shouldNotBeNull().let { it.jsonArray[0] @@ -369,15 +347,11 @@ class JsonPathUnitTest : FreeSpec({ "2.3.3. Index Selector" - { val jsonElement = Json.decodeFromString("[\"a\",\"b\"]") - "\$[1]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$[1]", jsonElement) { nodeList -> nodeList shouldHaveSize 1 jsonElement.jsonArray[1].shouldNotBeNull().shouldBeIn(nodeList) } - "\$[-2]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$[-2]", jsonElement) { nodeList -> nodeList shouldHaveSize 1 jsonElement.jsonArray[0].shouldNotBeNull().shouldBeIn(nodeList) } @@ -387,9 +361,7 @@ class JsonPathUnitTest : FreeSpec({ val jsonElement = Json.decodeFromString("[\"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"g\"]") - "\$[1:3]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$[1:3]", jsonElement) { nodeList -> nodeList shouldHaveSize 2 jsonElement.jsonArray[1].shouldNotBeNull().shouldBeIn(nodeList).let { nodeList[0].shouldBe(it) @@ -398,9 +370,7 @@ class JsonPathUnitTest : FreeSpec({ nodeList[1].shouldBe(it) } } - "\$[5:]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$[5:]", jsonElement) { nodeList -> nodeList shouldHaveSize 2 jsonElement.jsonArray[5].shouldNotBeNull().shouldBeIn(nodeList).let { nodeList[0].shouldBe(it) @@ -409,9 +379,7 @@ class JsonPathUnitTest : FreeSpec({ nodeList[1].shouldBe(it) } } - "\$[1:5:2]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$[1:5:2]", jsonElement) { nodeList -> nodeList shouldHaveSize 2 jsonElement.jsonArray[1].shouldNotBeNull().shouldBeIn(nodeList).let { nodeList[0].shouldBe(it) @@ -420,9 +388,7 @@ class JsonPathUnitTest : FreeSpec({ nodeList[1].shouldBe(it) } } - "\$[5:1:-2]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$[5:1:-2]", jsonElement) { nodeList -> nodeList shouldHaveSize 2 jsonElement.jsonArray[5].shouldNotBeNull().shouldBeIn(nodeList).let { nodeList[0].shouldBe(it) @@ -431,9 +397,7 @@ class JsonPathUnitTest : FreeSpec({ nodeList[1].shouldBe(it) } } - "\$[::-1]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$[::-1]", jsonElement) { nodeList -> nodeList shouldHaveSize 7 for (i in 6.downTo(0)) { jsonElement.jsonArray[i].shouldNotBeNull().shouldBeIn(nodeList).let { @@ -455,144 +419,88 @@ class JsonPathUnitTest : FreeSpec({ " }" ).jsonObject - "\$[?\$.absent1 == \$.absent2]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$[?\$.absent1 == \$.absent2]", jsonElement) { nodeList -> nodeList shouldHaveSize jsonElement.size } - "\$[?\$.absent1 <= \$.absent2 ]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$[?\$.absent1 <= \$.absent2 ]", jsonElement) { nodeList -> nodeList shouldHaveSize jsonElement.size } - "\$[?\$.absent == 'g']" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$[?\$.absent == 'g']", jsonElement) { nodeList -> nodeList shouldHaveSize 0 } - "\$[?\$.absent1 != \$.absent2]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$[?\$.absent1 != \$.absent2]", jsonElement) { nodeList -> nodeList shouldHaveSize 0 } - "\$[?\$.absent != 'g']" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$[?\$.absent != 'g']", jsonElement) { nodeList -> nodeList shouldHaveSize jsonElement.size } - "\$[?1 <= 2]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$[?1 <= 2]", jsonElement) { nodeList -> nodeList shouldHaveSize jsonElement.size } - "\$[?1 > 2]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$[?1 > 2]", jsonElement) { nodeList -> nodeList shouldHaveSize 0 } - "\$[?13 == '13']" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$[?13 == '13']", jsonElement) { nodeList -> nodeList shouldHaveSize 0 } - "\$[?'a' <= 'b']" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$[?'a' <= 'b']", jsonElement) { nodeList -> nodeList shouldHaveSize jsonElement.size } - "\$[?'a' > 'b']" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$[?'a' > 'b']", jsonElement) { nodeList -> nodeList shouldHaveSize 0 } - "\$[?\$.obj == \$.arr]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$[?\$.obj == \$.arr]", jsonElement) { nodeList -> nodeList shouldHaveSize 0 } - "\$[?\$.obj != \$.arr]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$[?\$.obj != \$.arr]", jsonElement) { nodeList -> nodeList shouldHaveSize jsonElement.size } - "\$[?\$.obj == \$.obj]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$[?\$.obj == \$.obj]", jsonElement) { nodeList -> nodeList shouldHaveSize jsonElement.size } - "\$[?\$.obj != \$.obj]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$[?\$.obj != \$.obj]", jsonElement) { nodeList -> nodeList shouldHaveSize 0 } - "\$[?\$.arr == \$.arr]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$[?\$.arr == \$.arr]", jsonElement) { nodeList -> nodeList shouldHaveSize jsonElement.size } - "\$[?\$.arr != \$.arr]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$[?\$.arr != \$.arr]", jsonElement) { nodeList -> nodeList shouldHaveSize 0 } - "\$[?\$.obj == 17]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$[?\$.obj == 17]", jsonElement) { nodeList -> nodeList shouldHaveSize 0 } - "\$[?\$.obj != 17]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$[?\$.obj != 17]", jsonElement) { nodeList -> nodeList shouldHaveSize jsonElement.size } - "\$[?\$.obj <= \$.arr]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$[?\$.obj <= \$.arr]", jsonElement) { nodeList -> nodeList shouldHaveSize 0 } - "\$[?\$.obj < \$.arr]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$[?\$.obj < \$.arr]", jsonElement) { nodeList -> nodeList shouldHaveSize 0 } - "\$[?\$.obj <= \$.obj]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$[?\$.obj <= \$.obj]", jsonElement) { nodeList -> nodeList shouldHaveSize jsonElement.size } - "\$[?\$.arr <= \$.arr]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$[?\$.arr <= \$.arr]", jsonElement) { nodeList -> nodeList shouldHaveSize jsonElement.size } - "\$[?1 <= \$.arr]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$[?1 <= \$.arr]", jsonElement) { nodeList -> nodeList shouldHaveSize 0 } - "\$[?1 >= \$.arr]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$[?1 >= \$.arr]", jsonElement) { nodeList -> nodeList shouldHaveSize 0 } - "\$[?1 > \$.arr]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$[?1 > \$.arr]", jsonElement) { nodeList -> nodeList shouldHaveSize 0 } - "\$[?1 < \$.arr]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$[?1 < \$.arr]", jsonElement) { nodeList -> nodeList shouldHaveSize 0 } - "\$[?true <= true]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$[?true <= true]", jsonElement) { nodeList -> nodeList shouldHaveSize jsonElement.size } - "\$[?true > true]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$[?true > true]", jsonElement) { nodeList -> nodeList shouldHaveSize 0 } } @@ -610,27 +518,17 @@ class JsonPathUnitTest : FreeSpec({ " }" ).jsonObject - - "\$.a[?@.b == 'kilo']" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } - + query("\$.a[?@.b == 'kilo']", jsonElement) { nodeList -> nodeList shouldHaveSize 1 jsonElement.jsonObject["a"].shouldNotBeNull() .jsonArray.get(9).shouldNotBeNull().shouldBeIn(nodeList) } - "\$.a[?(@.b == 'kilo')]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } - + query("\$.a[?(@.b == 'kilo')]", jsonElement) { nodeList -> nodeList shouldHaveSize 1 jsonElement.jsonObject["a"].shouldNotBeNull() .jsonArray.get(9).shouldNotBeNull().shouldBeIn(nodeList) } - "\$.a[?@>3.5]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } - + query("\$.a[?@>3.5]", jsonElement) { nodeList -> nodeList shouldHaveSize 3 jsonElement.jsonObject["a"].shouldNotBeNull().let { a -> a.jsonArray[1].shouldNotBeNull().shouldBeIn(nodeList) @@ -638,10 +536,7 @@ class JsonPathUnitTest : FreeSpec({ a.jsonArray[5].shouldNotBeNull().shouldBeIn(nodeList) } } - "\$.a[?@.b]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } - + query("\$.a[?@.b]", jsonElement) { nodeList -> nodeList shouldHaveSize 4 jsonElement.jsonObject["a"].shouldNotBeNull().let { a -> a.jsonArray[6].shouldNotBeNull().shouldBeIn(nodeList) @@ -650,25 +545,16 @@ class JsonPathUnitTest : FreeSpec({ a.jsonArray[9].shouldNotBeNull().shouldBeIn(nodeList) } } - "\$[?@.*]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } - + query("\$[?@.*]", jsonElement) { nodeList -> nodeList shouldHaveSize 2 jsonElement.jsonObject["a"].shouldNotBeNull().shouldBeIn(nodeList) jsonElement.jsonObject["o"].shouldNotBeNull().shouldBeIn(nodeList) } - "\$[?@[?@.b]]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } - + query("\$[?@[?@.b]]", jsonElement) { nodeList -> nodeList shouldHaveSize 1 jsonElement.jsonObject["a"].shouldNotBeNull().shouldBeIn(nodeList) } - "\$.o[?@<3, ?@<3]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } - + query("\$.o[?@<3, ?@<3]", jsonElement) { nodeList -> nodeList shouldHaveSize 4 jsonElement.jsonObject["o"].shouldNotBeNull().jsonObject.let { o -> o["p"].shouldNotBeNull().shouldBeIn(nodeList).let { p -> @@ -683,30 +569,21 @@ class JsonPathUnitTest : FreeSpec({ } } } - "\$.a[?@<2 || @.b == \"k\"]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } - + query("\$.a[?@<2 || @.b == \"k\"]", jsonElement) { nodeList -> nodeList shouldHaveSize 2 jsonElement.jsonObject["a"].shouldNotBeNull().let { a -> a.jsonArray.get(2).shouldNotBeNull().shouldBeIn(nodeList) a.jsonArray.get(7).shouldNotBeNull().shouldBeIn(nodeList) } } - "\$.a[?match(@.b, \"[jk]\")]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } - + query("\$.a[?match(@.b, \"[jk]\")]", jsonElement) { nodeList -> nodeList shouldHaveSize 2 jsonElement.jsonObject["a"].shouldNotBeNull().let { a -> a.jsonArray.get(6).shouldNotBeNull().shouldBeIn(nodeList) a.jsonArray.get(7).shouldNotBeNull().shouldBeIn(nodeList) } } - "\$.a[?search(@.b, \"[jk]\")]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } - + query("\$.a[?search(@.b, \"[jk]\")]", jsonElement) { nodeList -> nodeList shouldHaveSize 3 jsonElement.jsonObject["a"].shouldNotBeNull().let { a -> a.jsonArray.get(6).shouldNotBeNull().shouldBeIn(nodeList) @@ -714,28 +591,19 @@ class JsonPathUnitTest : FreeSpec({ a.jsonArray.get(9).shouldNotBeNull().shouldBeIn(nodeList) } } - "\$.o[?@>1 && @<4]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } - + query("\$.o[?@>1 && @<4]", jsonElement) { nodeList -> nodeList shouldHaveSize 2 jsonElement.jsonObject["o"].shouldNotBeNull().jsonObject.let { o -> o["q"].shouldNotBeNull().shouldBeIn(nodeList) o["r"].shouldNotBeNull().shouldBeIn(nodeList) } } - "\$.o[?@.u || @.x]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } - + query("\$.o[?@.u || @.x]", jsonElement) { nodeList -> nodeList shouldHaveSize 1 jsonElement.jsonObject["o"].shouldNotBeNull().jsonObject .get("t").shouldNotBeNull().shouldBeIn(nodeList) } - "\$.a[?@.b == $.x]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } - + query("\$.a[?@.b == $.x]", jsonElement) { nodeList -> nodeList shouldHaveSize 6 jsonElement.jsonObject["a"].shouldNotBeNull().jsonArray.let { a -> nodeList[0].shouldBe(a[0]) @@ -746,10 +614,7 @@ class JsonPathUnitTest : FreeSpec({ nodeList[5].shouldBe(a[5]) } } - "\$.a[?@ || @]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } - + query("\$.a[?@ || @]", jsonElement) { nodeList -> nodeList shouldHaveSize 10 jsonElement.jsonObject["a"].shouldNotBeNull().jsonArray.let { a -> for (index in 0..9) { @@ -760,119 +625,133 @@ class JsonPathUnitTest : FreeSpec({ } } - "2.4. Function Extensions" - { - // making sure that the dependencies are reset to their default for the next test - val defaultCompilerBuilderBackup = JsonPath.defaultCompiler - val defaultFunctionExtensionRepositoryBackup = - JsonPath.defaultFunctionExtensionRepository.export() - beforeEach { + "2.4. Function Extensions"( + TestConfig.aroundEachTest { action -> // prepare a dummy repository to be modified by the tests - JsonPath.defaultFunctionExtensionRepository = - JsonPathFunctionExtensionMapRepository( + JsonPath.defaultFunctionExtensionRepository = JsonPathFunctionExtensionMapRepository( + defaultFunctionExtensionRepositoryBackup.toMutableMap() + ) + try { + action() + } finally { + JsonPath.defaultCompiler = defaultCompilerBuilderBackup + JsonPath.defaultFunctionExtensionRepository = JsonPathFunctionExtensionMapRepository( defaultFunctionExtensionRepositoryBackup.toMutableMap() ) - } - afterEach { - JsonPath.Companion.apply { - defaultCompiler = defaultCompilerBuilderBackup - defaultFunctionExtensionRepository = - JsonPathFunctionExtensionMapRepository( - defaultFunctionExtensionRepositoryBackup.toMutableMap() - ) - } - } - "\$[?length(@) < 3]" { - shouldNotThrowAny { - JsonPath(this.testScope.testCase.name.originalName) - } - } - "\$[?length(@.*) < 3]" { - shouldThrow { - JsonPath(this.testScope.testCase.name.originalName) - } - } - "\$[?count(@.*) == 1]" { - shouldNotThrowAny { - JsonPath(this.testScope.testCase.name.originalName) - } - } - "\$[?count(1) == 1]" { - shouldThrow { - JsonPath(this.testScope.testCase.name.originalName) - } - } - "\$[?count(foo(@.*)) == 1]" { - JsonPath.defaultFunctionExtensionRepository.addExtension("foo") { - JsonPathFunctionExtension.NodesTypeFunctionExtension( - JsonPathFilterExpressionType.NodesType, - ) { - listOf() - } - } - - shouldNotThrowAny { - JsonPath(this.testScope.testCase.name.originalName) } } - "\$[?match(@.timezone, 'Europe/.*') == true]" { - shouldThrow { - JsonPath(this.testScope.testCase.name.originalName) - } - } - "\$[?value(@..color) == 'red']" { - shouldNotThrowAny { - JsonPath(this.testScope.testCase.name.originalName) - } - } - "\$[?value(@..color)]" { - shouldThrow { - JsonPath(this.testScope.testCase.name.originalName) - } - } - "\$[?bar(@.a)]" - { - "logical type argument" { - JsonPath.defaultFunctionExtensionRepository.addExtension("bar") { - JsonPathFunctionExtension.LogicalTypeFunctionExtension( - JsonPathFilterExpressionType.LogicalType, + ) - { + compiles("\$[?length(@) < 3]") + failsTypeCheck("\$[?length(@.*) < 3]") + compiles("\$[?count(@.*) == 1]") + failsTypeCheck("\$[?count(1) == 1]") + "\$[?count(foo(@.*)) == 1]".let { path -> + path { + JsonPath.defaultFunctionExtensionRepository.addExtension("foo") { + JsonPathFunctionExtension.NodesTypeFunctionExtension( + JsonPathFilterExpressionType.NodesType, ) { - true + listOf() } } shouldNotThrowAny { - JsonPath(this.testScope.testCase.parent!!.name.originalName) + JsonPath(path) } } - "value type argument" { - JsonPath.defaultFunctionExtensionRepository.addExtension("bar") { - JsonPathFunctionExtension.LogicalTypeFunctionExtension( - JsonPathFilterExpressionType.ValueType, - ) { - true + } + failsTypeCheck("\$[?match(@.timezone, 'Europe/.*') == true]") + compiles("\$[?value(@..color) == 'red']") + failsTypeCheck("\$[?value(@..color)]") + "\$[?bar(@.a)]".let { path -> + path - { + "logical type argument" { + JsonPath.defaultFunctionExtensionRepository.addExtension("bar") { + JsonPathFunctionExtension.LogicalTypeFunctionExtension( + JsonPathFilterExpressionType.LogicalType, + ) { + true + } + } + + shouldNotThrowAny { + JsonPath(path) } } + "value type argument" { + JsonPath.defaultFunctionExtensionRepository.addExtension("bar") { + JsonPathFunctionExtension.LogicalTypeFunctionExtension( + JsonPathFilterExpressionType.ValueType, + ) { + true + } + } - shouldNotThrowAny { - JsonPath(this.testScope.testCase.parent!!.name.originalName) + shouldNotThrowAny { + JsonPath(path) + } + } + "nodes type argument" { + JsonPath.defaultFunctionExtensionRepository.addExtension("bar") { + JsonPathFunctionExtension.LogicalTypeFunctionExtension( + JsonPathFilterExpressionType.NodesType, + ) { + true + } + } + + shouldNotThrowAny { + JsonPath(path) + } } } - "nodes type argument" { - JsonPath.defaultFunctionExtensionRepository.addExtension("bar") { - JsonPathFunctionExtension.LogicalTypeFunctionExtension( - JsonPathFilterExpressionType.NodesType, - ) { - true + } + "\$[?bnl(@.*)]".let { path -> + path - { + "logical type argument" { + JsonPath.defaultFunctionExtensionRepository.addExtension("bnl") { + JsonPathFunctionExtension.LogicalTypeFunctionExtension( + JsonPathFilterExpressionType.LogicalType, + ) { + true + } + } + + shouldNotThrowAny { + JsonPath(path) } } + "value type argument" { + JsonPath.defaultFunctionExtensionRepository.addExtension("bnl") { + JsonPathFunctionExtension.LogicalTypeFunctionExtension( + JsonPathFilterExpressionType.ValueType, + ) { + true + } + } - shouldNotThrowAny { - JsonPath(this.testScope.testCase.parent!!.name.originalName) + shouldThrow { + JsonPath(path) + } + } + "nodes type argument" { + JsonPath.defaultFunctionExtensionRepository.addExtension("bnl") { + JsonPathFunctionExtension.LogicalTypeFunctionExtension( + JsonPathFilterExpressionType.NodesType + ) { + true + } + } + + shouldNotThrowAny { + JsonPath(path) + } } } } - "\$[?bnl(@.*)]" - { - "logical type argument" { - JsonPath.defaultFunctionExtensionRepository.addExtension("bnl") { + "\$[?blt(1==1)]".let { path -> + path { + JsonPath.defaultFunctionExtensionRepository.addExtension("blt") { JsonPathFunctionExtension.LogicalTypeFunctionExtension( JsonPathFilterExpressionType.LogicalType, ) { @@ -881,74 +760,39 @@ class JsonPathUnitTest : FreeSpec({ } shouldNotThrowAny { - JsonPath(this.testScope.testCase.parent!!.name.originalName) + JsonPath(path) } } - "value type argument" { - JsonPath.defaultFunctionExtensionRepository.addExtension("bnl") { + } + "\$[?blt(1)]".let { path -> + path { + JsonPath.defaultFunctionExtensionRepository.addExtension("blt") { JsonPathFunctionExtension.LogicalTypeFunctionExtension( - JsonPathFilterExpressionType.ValueType, + JsonPathFilterExpressionType.LogicalType, ) { true } } shouldThrow { - JsonPath(this.testScope.testCase.parent!!.name.originalName) + JsonPath(path) } } - "nodes type argument" { - JsonPath.defaultFunctionExtensionRepository.addExtension("bnl") { + } + "\$[?bal(1)]".let { path -> + path { + JsonPath.defaultFunctionExtensionRepository.addExtension("bal") { JsonPathFunctionExtension.LogicalTypeFunctionExtension( - JsonPathFilterExpressionType.NodesType + JsonPathFilterExpressionType.ValueType, ) { true } } shouldNotThrowAny { - JsonPath(this.testScope.testCase.parent!!.name.originalName) - } - } - } - "\$[?blt(1==1)]" { - JsonPath.defaultFunctionExtensionRepository.addExtension("blt") { - JsonPathFunctionExtension.LogicalTypeFunctionExtension( - JsonPathFilterExpressionType.LogicalType, - ) { - true + JsonPath(path) } } - - shouldNotThrowAny { - JsonPath(this.testScope.testCase.name.originalName) - } - } - "\$[?blt(1)]" { - JsonPath.defaultFunctionExtensionRepository.addExtension("blt") { - JsonPathFunctionExtension.LogicalTypeFunctionExtension( - JsonPathFilterExpressionType.LogicalType, - ) { - true - } - } - - shouldThrow { - JsonPath(this.testScope.testCase.name.originalName) - } - } - "\$[?bal(1)]" { - JsonPath.defaultFunctionExtensionRepository.addExtension("bal") { - JsonPathFunctionExtension.LogicalTypeFunctionExtension( - JsonPathFilterExpressionType.ValueType, - ) { - true - } - } - - shouldNotThrowAny { - JsonPath(this.testScope.testCase.name.originalName) - } } } @@ -956,27 +800,18 @@ class JsonPathUnitTest : FreeSpec({ val jsonElement = Json.decodeFromString( "[\"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"g\"]" ).jsonArray - "\$[0, 3]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } - + query("\$[0, 3]", jsonElement) { nodeList -> nodeList shouldHaveSize 2 nodeList[0].shouldBe(jsonElement[0]) nodeList[1].shouldBe(jsonElement[3]) } - "\$[0:2,5]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } - + query("\$[0:2,5]", jsonElement) { nodeList -> nodeList shouldHaveSize 3 nodeList[0].shouldBe(jsonElement[0]) nodeList[1].shouldBe(jsonElement[1]) nodeList[2].shouldBe(jsonElement[5]) } - "\$[0,0]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } - + query("\$[0,0]", jsonElement) { nodeList -> nodeList shouldHaveSize 2 nodeList[0].shouldBe(jsonElement[0]) nodeList[1].shouldBe(jsonElement[0]) @@ -990,10 +825,7 @@ class JsonPathUnitTest : FreeSpec({ " \"a\": [5, 3, [{\"j\": 4}, {\"k\": 6}]]\n" + " }" ).jsonObject - "\$..j" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } - + query("\$..j", jsonElement) { nodeList -> nodeList shouldHaveSize 2 jsonElement["o"].shouldNotBeNull().jsonObject["j"].shouldBeIn(nodeList) jsonElement["a"].shouldNotBeNull() @@ -1001,20 +833,14 @@ class JsonPathUnitTest : FreeSpec({ .jsonArray[0].shouldNotBeNull() .jsonObject["j"].shouldNotBeNull().shouldBeIn(nodeList) } - "\$..[0]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } - + query("\$..[0]", jsonElement) { nodeList -> nodeList shouldHaveSize 2 jsonElement["a"].shouldNotBeNull().jsonArray.let { a -> a.jsonArray[0].shouldNotBeNull().shouldBe(nodeList[0]) a.jsonArray[2].shouldNotBeNull().jsonArray[0].shouldBe(nodeList[1]) } } - "\$..[*]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } - + query("\$..[*]", jsonElement) { nodeList -> nodeList shouldHaveSize 11 jsonElement["a"].shouldNotBeNull().shouldBeIn(nodeList).jsonArray.let { a -> a.forEach { item -> @@ -1035,10 +861,7 @@ class JsonPathUnitTest : FreeSpec({ } } } - "\$..*" { // same as the one before this - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } - + query("\$..*", jsonElement) { nodeList -> // same as the one before this nodeList shouldHaveSize 11 jsonElement["a"].shouldNotBeNull().shouldBeIn(nodeList).jsonArray.let { a -> a.forEach { item -> @@ -1059,17 +882,11 @@ class JsonPathUnitTest : FreeSpec({ } } } - "\$..o" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } - + query("\$..o", jsonElement) { nodeList -> nodeList shouldHaveSize 1 jsonElement["o"].shouldNotBeNull().shouldBeIn(nodeList) } - "\$.o..[*, *]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } - + query("\$.o..[*, *]", jsonElement) { nodeList -> nodeList shouldHaveSize 4 jsonElement["o"].shouldNotBeNull().jsonObject.let { o -> o.forEach { oDescendant -> @@ -1080,10 +897,7 @@ class JsonPathUnitTest : FreeSpec({ } } } - "\$.a..[0, 1]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } - + query("\$.a..[0, 1]", jsonElement) { nodeList -> nodeList shouldHaveSize 4 jsonElement["a"].shouldNotBeNull().jsonArray.let { a -> listOf(a, a[2]).forEach { descendant -> @@ -1098,69 +912,42 @@ class JsonPathUnitTest : FreeSpec({ val jsonElement = Json.decodeFromString( " {\"a\": null, \"b\": [null], \"c\": [{}], \"null\": 1}" ).jsonObject - "\$.a" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } - + query("\$.a", jsonElement) { nodeList -> nodeList shouldHaveSize 1 jsonElement["a"].shouldNotBeNull().shouldBeIn(nodeList) } - "\$.a[0]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } - + query("\$.a[0]", jsonElement) { nodeList -> nodeList shouldHaveSize 0 } - "\$.a.d" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } - + query("\$.a.d", jsonElement) { nodeList -> nodeList shouldHaveSize 0 } - "\$.b[0]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } - + query("\$.b[0]", jsonElement) { nodeList -> nodeList shouldHaveSize 1 jsonElement["b"].shouldNotBeNull().jsonArray[0].shouldBeIn(nodeList) } - "\$.b[*]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } - + query("\$.b[*]", jsonElement) { nodeList -> nodeList shouldHaveSize 1 jsonElement["b"].shouldNotBeNull().jsonArray.mapIndexed { index, value -> nodeList[index].shouldBe(value) } } - "\$.b[?@]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } - + query("\$.b[?@]", jsonElement) { nodeList -> nodeList shouldHaveSize 1 jsonElement["b"].shouldNotBeNull().jsonArray.mapIndexed { index, value -> nodeList[index].shouldBe(value) } } - "\$.b[?@==null]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } - + query("\$.b[?@==null]", jsonElement) { nodeList -> nodeList shouldHaveSize 1 jsonElement["b"].shouldNotBeNull().jsonArray.mapIndexed { index, value -> nodeList[index].shouldBe(value) } } - "\$.c[?@.d==null]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } - + query("\$.c[?@.d==null]", jsonElement) { nodeList -> nodeList shouldHaveSize 0 } - "\$.null" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } - + query("\$.null", jsonElement) { nodeList -> nodeList shouldHaveSize 1 jsonElement["null"].shouldNotBeNull().shouldBeIn(nodeList) } @@ -1206,52 +993,36 @@ class JsonPathUnitTest : FreeSpec({ ) "working" - { - "$[?$.tag.id == 'ic2:geo_generator' || $.tag.id == 'ic2:generator']" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("$[?$.tag.id == 'ic2:geo_generator' || $.tag.id == 'ic2:generator']", jsonElement) { nodeList -> nodeList shouldHaveSize 5 } - "\$[?(\$.tag.id == 'ic2:geo_generator' || \$.tag.id == 'ic2:generator')]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$[?(\$.tag.id == 'ic2:geo_generator' || \$.tag.id == 'ic2:generator')]", jsonElement) { nodeList -> nodeList shouldHaveSize 5 } - "\$.tag[?(\$.tag.id == 'ic2:geo_generator' || \$.tag.id == 'ic2:generator')]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$.tag[?(\$.tag.id == 'ic2:geo_generator' || \$.tag.id == 'ic2:generator')]", jsonElement) { nodeList -> nodeList shouldHaveSize 10 } } "not working" - { - "\$.tag.id[?(\$.tag.id == 'ic2:geo_generator' || \$.tag.id == 'ic2:generator')]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$.tag.id[?(\$.tag.id == 'ic2:geo_generator' || \$.tag.id == 'ic2:generator')]", jsonElement) { nodeList -> nodeList shouldHaveSize 0 } - "\$[?(@.tag.id == 'ic2:geo_generator' || @.tag.id == 'ic2:generator')]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$[?(@.tag.id == 'ic2:geo_generator' || @.tag.id == 'ic2:generator')]", jsonElement) { nodeList -> nodeList shouldHaveSize 0 } - "\$.tag[?(@.id == 'ic2:geo_generator' || @.id == 'ic2:generator')]" { - val nodeList = JsonPath(this.testScope.testCase.name.originalName) - .query(jsonElement).map { it.value } + query("\$.tag[?(@.id == 'ic2:geo_generator' || @.id == 'ic2:generator')]", jsonElement) { nodeList -> nodeList shouldHaveSize 0 } } } "RMLTC0002g-JSON" - { - "$.students[*]]" { - shouldThrow { - JsonPath(this.testScope.testCase.name.originalName) - } - } + failsParsing("$.students[*]]") } } -}) +} diff --git a/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/MemberNameShorthandSerializationTest.kt b/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/MemberNameShorthandSerializationTest.kt index a6c2e10..4ee5176 100644 --- a/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/MemberNameShorthandSerializationTest.kt +++ b/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/MemberNameShorthandSerializationTest.kt @@ -1,35 +1,38 @@ package at.asitplus.jsonpath import at.asitplus.jsonpath.core.NormalizedJsonPathSegment +import at.asitplus.testballoon.matrix.matrixSuite import io.kotest.assertions.throwables.shouldNotThrowAny import io.kotest.assertions.throwables.shouldThrowAny -import io.kotest.core.spec.style.FreeSpec -import io.kotest.datatest.withData import io.kotest.matchers.shouldBe -class MemberNameShorthandSerializationTest : FreeSpec({ +val MemberNameShorthandSerializationTest by matrixSuite { "should be serializable as member name shorthand" - { - withData( - "test", - "test_123", - "t1", - ) { + data( + listOf( + "test", + "test_123", + "t1", + ) + ) test { shouldNotThrowAny { NormalizedJsonPathSegment.NameSegment(it).toShorthandNotation() } shouldBe ".$it" } } "should not be serializable as member name shorthand" - { - withData( - "1", - "*", - "'", - "\"", - "test-data", - ) { + data( + listOf( + "1", + "*", + "'", + "\"", + "test-data", + ) + ) test { shouldThrowAny { NormalizedJsonPathSegment.NameSegment(it).toShorthandNotation() shouldBe ".$it" } } } -}) \ No newline at end of file +} diff --git a/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/NodeListSerializationTest.kt b/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/NodeListSerializationTest.kt index 90d02d5..b0eeebc 100644 --- a/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/NodeListSerializationTest.kt +++ b/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/NodeListSerializationTest.kt @@ -3,7 +3,7 @@ package at.asitplus.jsonpath import at.asitplus.jsonpath.core.NodeListEntry import at.asitplus.jsonpath.core.NormalizedJsonPath import at.asitplus.jsonpath.core.NormalizedJsonPathSegment -import io.kotest.core.spec.style.FreeSpec +import at.asitplus.testballoon.matrix.matrixSuite import io.kotest.matchers.shouldBe import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json @@ -11,8 +11,7 @@ import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.buildJsonArray import kotlinx.serialization.json.buildJsonObject -@Suppress("unused") -class NodeListSerializationTest : FreeSpec({ +val NodeListSerializationTest by matrixSuite { "NormalizedJsonPathSegment" - { "NameSegment" { val segment: NormalizedJsonPathSegment = NormalizedJsonPathSegment.NameSegment("test") @@ -93,7 +92,5 @@ class NodeListSerializationTest : FreeSpec({ reconstructed.normalizedJsonPath.toString() shouldBe entry.normalizedJsonPath.toString() } } - "NodeList" - { - // trusting the default serializer for list for this one - } -}) \ No newline at end of file + // "NodeList": trusting the default serializer for list for this one (no test) +} diff --git a/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/NormalizedJsonPathSerializationTest.kt b/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/NormalizedJsonPathSerializationTest.kt index 2dd7b8d..abd4c11 100644 --- a/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/NormalizedJsonPathSerializationTest.kt +++ b/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/NormalizedJsonPathSerializationTest.kt @@ -2,32 +2,35 @@ package at.asitplus.jsonpath import at.asitplus.jsonpath.core.NormalizedJsonPath import at.asitplus.jsonpath.core.NormalizedJsonPathSegment +import at.asitplus.testballoon.matrix.matrixSuite import io.kotest.assertions.throwables.shouldNotThrowAny import io.kotest.assertions.throwables.shouldThrowAny -import io.kotest.core.spec.style.FreeSpec -import io.kotest.datatest.withData import io.kotest.matchers.shouldBe -class NormalizedJsonPathSerializationTest : FreeSpec({ +val NormalizedJsonPathSerializationTest by matrixSuite { "normalized json path string" - { - withData( - "test" to "$['test']", - "test_123" to "$['test_123']", - "t1" to "$['t1']", - ) { (it, expected) -> + data( + listOf( + "test" to "$['test']", + "test_123" to "$['test_123']", + "t1" to "$['t1']", + ) + ) test { (input, expected) -> shouldNotThrowAny { - val path = NormalizedJsonPath() + NormalizedJsonPathSegment.NameSegment(it) + val path = NormalizedJsonPath() + NormalizedJsonPathSegment.NameSegment(input) path.toNormalizedJsonPathString() shouldBe expected } } } "shorthand serialization" - { "should be serializable as member name shorthand" - { - withData( - "test", - "test_123", - "t1", - ) { + data( + listOf( + "test", + "test_123", + "t1", + ) + ) test { shouldNotThrowAny { val path = NormalizedJsonPath() + NormalizedJsonPathSegment.NameSegment(it) path.toShorthandNameSegmentNotation() shouldBe "$.$it" @@ -36,13 +39,15 @@ class NormalizedJsonPathSerializationTest : FreeSpec({ } } "should not be serializable as member name shorthand" - { - withData( - "1", - "*", - "'", - "\"", - "test-data", - ) { + data( + listOf( + "1", + "*", + "'", + "\"", + "test-data", + ) + ) test { val path = NormalizedJsonPath() + NormalizedJsonPathSegment.NameSegment(it) shouldThrowAny { path.toShorthandNameSegmentNotation() @@ -51,4 +56,4 @@ class NormalizedJsonPathSerializationTest : FreeSpec({ } } } -}) \ No newline at end of file +} diff --git a/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/Rfc9535UtilsUnitTest.kt b/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/Rfc9535UtilsUnitTest.kt index de6c76b..03a77e7 100644 --- a/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/Rfc9535UtilsUnitTest.kt +++ b/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/Rfc9535UtilsUnitTest.kt @@ -1,64 +1,45 @@ package at.asitplus.jsonpath import at.asitplus.jsonpath.core.Rfc9535Utils -import io.kotest.core.spec.style.FreeSpec +import at.asitplus.testballoon.matrix.matrixSuite import io.kotest.matchers.shouldBe -@Suppress("unused") -class Rfc9535UtilsUnitTest : FreeSpec({ +val Rfc9535UtilsUnitTest by matrixSuite { "Rfc9535Utils.unpackStringLiteral Unit Tests" - { "rfc8259 conformance" - { "special escape characters" - { "double quoted" - { - "unescapes backslash to backslash" { - Rfc9535Utils.unpackStringLiteral("\"\\\\\"") shouldBe "\\" - } - "unescapes slash to slash" { - Rfc9535Utils.unpackStringLiteral("\"\\/\"") shouldBe "/" - } - "unescapes quotation mark to quotation mark" { - Rfc9535Utils.unpackStringLiteral("\"\\\"\"") shouldBe "\"" - } - "unescapes b to backspace" { - Rfc9535Utils.unpackStringLiteral("\"\\b\"") shouldBe Char(0x0008).toString() - } - "unescapes f to form feed" { - Rfc9535Utils.unpackStringLiteral("\"\\f\"") shouldBe Char(0x000C).toString() - } - "unescapes n to newline" { - Rfc9535Utils.unpackStringLiteral("\"\\n\"") shouldBe "\n" - } - "unescapes r to carriage return" { - Rfc9535Utils.unpackStringLiteral("\"\\r\"") shouldBe "\r" - } - "unescapes t to horizontal tab" { - Rfc9535Utils.unpackStringLiteral("\"\\t\"") shouldBe "\t" + data( + listOf( + "\"\\\\\"" to "\\", + "\"\\/\"" to "/", + "\"\\\"\"" to "\"", + "\"\\b\"" to Char(0x0008).toString(), + "\"\\f\"" to Char(0x000C).toString(), + "\"\\n\"" to "\n", + "\"\\r\"" to "\r", + "\"\\t\"" to "\t", + ), + nameFn = { (input, _) -> input }, + ) test { (input, expected) -> + Rfc9535Utils.unpackStringLiteral(input) shouldBe expected } } "single quoted" - { - "unescapes backslash to backslash" { - Rfc9535Utils.unpackStringLiteral("'\\\\'") shouldBe "\\" - } - "unescapes slash to slash" { - Rfc9535Utils.unpackStringLiteral("'\\/'") shouldBe "/" - } - "unescapes quotation mark to quotation mark" { - Rfc9535Utils.unpackStringLiteral("'\\''") shouldBe "'" - } - "unescapes b to backspace" { - Rfc9535Utils.unpackStringLiteral("'\\b'") shouldBe Char(0x0008).toString() - } - "unescapes f to form feed" { - Rfc9535Utils.unpackStringLiteral("'\\f'") shouldBe Char(0x000C).toString() - } - "unescapes n to newline" { - Rfc9535Utils.unpackStringLiteral("'\\n'") shouldBe "\n" - } - "unescapes r to carriage return" { - Rfc9535Utils.unpackStringLiteral("'\\r'") shouldBe "\r" - } - "unescapes t to horizontal tab" { - Rfc9535Utils.unpackStringLiteral("'\\t'") shouldBe "\t" + data( + listOf( + "'\\\\'" to "\\", + "'\\/'" to "/", + "'\\''" to "'", + "'\\b'" to Char(0x0008).toString(), + "'\\f'" to Char(0x000C).toString(), + "'\\n'" to "\n", + "'\\r'" to "\r", + "'\\t'" to "\t", + ), + nameFn = { (input, _) -> input }, + ) test { (input, expected) -> + Rfc9535Utils.unpackStringLiteral(input) shouldBe expected } } } @@ -67,77 +48,23 @@ class Rfc9535UtilsUnitTest : FreeSpec({ "Rfc9535Utils.switchToSingleQuotedString Unit Tests" - { "rfc8259 conformance" - { "special escape characters" - { - "\"\\\\\"" { - Rfc9535Utils.switchToSingleQuotedString(this.testScope.testCase.name.originalName) - .let { - val expectedResult = "'\\\\'" - it shouldBe expectedResult - Rfc9535Utils.switchToSingleQuotedString(it) shouldBe expectedResult - } - } - "\"\\/\"" { - Rfc9535Utils.switchToSingleQuotedString(this.testScope.testCase.name.originalName) - .let { - val expectedResult = "'\\/'" - it shouldBe expectedResult - Rfc9535Utils.switchToSingleQuotedString(it) shouldBe expectedResult - } - } - "\"\\\"\"" { - Rfc9535Utils.switchToSingleQuotedString(this.testScope.testCase.name.originalName) - .let { - val expectedResult = "'\"'" - it shouldBe expectedResult - Rfc9535Utils.switchToSingleQuotedString(it) shouldBe expectedResult - } - } - "\"'\"" { - Rfc9535Utils.switchToSingleQuotedString(this.testScope.testCase.name.originalName) - .let { - val expectedResult = "'\\''" - it shouldBe expectedResult - Rfc9535Utils.switchToSingleQuotedString(it) shouldBe expectedResult - } - } - "\"\\b\"" { - Rfc9535Utils.switchToSingleQuotedString(this.testScope.testCase.name.originalName) - .let { - val expectedResult = "'\\b'" - it shouldBe expectedResult - Rfc9535Utils.switchToSingleQuotedString(it) shouldBe expectedResult - } - } - "\"\\f\"" { - Rfc9535Utils.switchToSingleQuotedString(this.testScope.testCase.name.originalName) - .let { - val expectedResult = "'\\f'" - it shouldBe expectedResult - Rfc9535Utils.switchToSingleQuotedString(it) shouldBe expectedResult - } - } - "\"\\n\"" { - Rfc9535Utils.switchToSingleQuotedString(this.testScope.testCase.name.originalName) - .let { - val expectedResult = "'\\n'" - it shouldBe expectedResult - Rfc9535Utils.switchToSingleQuotedString(it) shouldBe expectedResult - } - } - "\"\\r\"" { - Rfc9535Utils.switchToSingleQuotedString(this.testScope.testCase.name.originalName) - .let { - val expectedResult = "'\\r'" - it shouldBe expectedResult - Rfc9535Utils.switchToSingleQuotedString(it) shouldBe expectedResult - } - } - "\"\\t\"" { - Rfc9535Utils.switchToSingleQuotedString(this.testScope.testCase.name.originalName) - .let { - val expectedResult = "'\\t'" - it shouldBe expectedResult - Rfc9535Utils.switchToSingleQuotedString(it) shouldBe expectedResult - } + data( + listOf( + "\"\\\\\"" to "'\\\\'", + "\"\\/\"" to "'\\/'", + "\"\\\"\"" to "'\"'", + "\"'\"" to "'\\''", + "\"\\b\"" to "'\\b'", + "\"\\f\"" to "'\\f'", + "\"\\n\"" to "'\\n'", + "\"\\r\"" to "'\\r'", + "\"\\t\"" to "'\\t'", + ), + nameFn = { (input, _) -> input }, + ) test { (input, expected) -> + Rfc9535Utils.switchToSingleQuotedString(input) shouldBe expected + // switching an already single-quoted string is idempotent + Rfc9535Utils.switchToSingleQuotedString(expected) shouldBe expected } } } @@ -145,79 +72,25 @@ class Rfc9535UtilsUnitTest : FreeSpec({ "Rfc9535Utils.switchToDoubleQuotedString Unit Tests" - { "rfc8259 conformance" - { "special escape characters" - { - "'\\\\'" { - Rfc9535Utils.switchToDoubleQuotedString(this.testScope.testCase.name.originalName) - .let { - val expectedResult = "\"\\\\\"" - it shouldBe expectedResult - Rfc9535Utils.switchToDoubleQuotedString(it) shouldBe expectedResult - } - } - "'\\/'" { - Rfc9535Utils.switchToDoubleQuotedString(this.testScope.testCase.name.originalName) - .let { - val expectedResult = "\"\\/\"" - it shouldBe expectedResult - Rfc9535Utils.switchToDoubleQuotedString(it) shouldBe expectedResult - } - } - "'\"'" { - Rfc9535Utils.switchToDoubleQuotedString(this.testScope.testCase.name.originalName) - .let { - val expectedResult = "\"\\\"\"" - it shouldBe expectedResult - Rfc9535Utils.switchToDoubleQuotedString(it) shouldBe expectedResult - } - } - "'\\''" { - Rfc9535Utils.switchToDoubleQuotedString(this.testScope.testCase.name.originalName) - .let { - val expectedResult = "\"'\"" - it shouldBe expectedResult - Rfc9535Utils.switchToDoubleQuotedString(it) shouldBe expectedResult - } - } - "'\\b'" { - Rfc9535Utils.switchToDoubleQuotedString(this.testScope.testCase.name.originalName) - .let { - val expectedResult = "\"\\b\"" - it shouldBe expectedResult - Rfc9535Utils.switchToDoubleQuotedString(it) shouldBe expectedResult - } - } - "'\\f'" { - Rfc9535Utils.switchToDoubleQuotedString(this.testScope.testCase.name.originalName) - .let { - val expectedResult = "\"\\f\"" - it shouldBe expectedResult - Rfc9535Utils.switchToDoubleQuotedString(it) shouldBe expectedResult - } - } - "'\\n'" { - Rfc9535Utils.switchToDoubleQuotedString(this.testScope.testCase.name.originalName) - .let { - val expectedResult = "\"\\n\"" - it shouldBe expectedResult - Rfc9535Utils.switchToDoubleQuotedString(it) shouldBe expectedResult - } - } - "'\\r'" { - Rfc9535Utils.switchToDoubleQuotedString(this.testScope.testCase.name.originalName) - .let { - val expectedResult = "\"\\r\"" - it shouldBe expectedResult - Rfc9535Utils.switchToDoubleQuotedString(it) shouldBe expectedResult - } - } - "'\\t'" { - Rfc9535Utils.switchToDoubleQuotedString(this.testScope.testCase.name.originalName) - .let { - val expectedResult = "\"\\t\"" - it shouldBe expectedResult - Rfc9535Utils.switchToDoubleQuotedString(it) shouldBe expectedResult - } + data( + listOf( + "'\\\\'" to "\"\\\\\"", + "'\\/'" to "\"\\/\"", + "'\"'" to "\"\\\"\"", + "'\\''" to "\"'\"", + "'\\b'" to "\"\\b\"", + "'\\f'" to "\"\\f\"", + "'\\n'" to "\"\\n\"", + "'\\r'" to "\"\\r\"", + "'\\t'" to "\"\\t\"", + ), + nameFn = { (input, _) -> input }, + ) test { (input, expected) -> + Rfc9535Utils.switchToDoubleQuotedString(input) shouldBe expected + // switching an already double-quoted string is idempotent + Rfc9535Utils.switchToDoubleQuotedString(expected) shouldBe expected } } } } -}) \ No newline at end of file +} diff --git a/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/core/NormalizedJsonPathTest.kt b/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/core/NormalizedJsonPathTest.kt index 9c4a357..fd153ef 100644 --- a/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/core/NormalizedJsonPathTest.kt +++ b/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/core/NormalizedJsonPathTest.kt @@ -1,15 +1,12 @@ package at.asitplus.jsonpath.core +import at.asitplus.testballoon.matrix.matrixSuite 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() { +val NormalizedJsonPathTest by matrixSuite { + "equality" { NormalizedJsonPath() shouldBe NormalizedJsonPath() NormalizedJsonPath("name") shouldBe NormalizedJsonPath() + "name" NormalizedJsonPath(1) shouldBe NormalizedJsonPath() + 1u @@ -19,8 +16,7 @@ class NormalizedJsonPathTest { NormalizedJsonPath() to "2", ) shouldHaveSize 1 } - @Test - fun inequality() { + "inequality" { NormalizedJsonPath(1) shouldNotBe NormalizedJsonPath() NormalizedJsonPath(1) shouldNotBe NormalizedJsonPath() + "1" NormalizedJsonPath(1) shouldNotBe NormalizedJsonPath() + 2 @@ -30,4 +26,4 @@ class NormalizedJsonPathTest { NormalizedJsonPath() shouldNotBe NormalizedJsonPath() + "1" NormalizedJsonPath() shouldNotBe NormalizedJsonPath() + 1 } -} \ No newline at end of file +} diff --git a/jsonpath4k/src/nativeTest/kotlin/at/asitplus/jsonpath/AntlrConcurrencyNativeTest.kt b/jsonpath4k/src/nativeTest/kotlin/at/asitplus/jsonpath/AntlrConcurrencyNativeTest.kt new file mode 100644 index 0000000..725f35f --- /dev/null +++ b/jsonpath4k/src/nativeTest/kotlin/at/asitplus/jsonpath/AntlrConcurrencyNativeTest.kt @@ -0,0 +1,198 @@ +@file:OptIn(DelicateCoroutinesApi::class) + +package at.asitplus.jsonpath + +import de.infix.testBalloon.framework.core.TestConfig +import de.infix.testBalloon.framework.core.testScope +import de.infix.testBalloon.framework.core.testSuite +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.newFixedThreadPoolContext +import kotlinx.coroutines.withTimeout +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlin.coroutines.CoroutineContext +import kotlin.time.Duration.Companion.seconds + +/** + * Highly-concurrent stress tests that hammer the ANTLR-generated lexer/parser (via [JsonPath]) from many + * threads at once. + * + * antlr-kotlin is **not** thread-safe: a generated `Lexer`/`Parser` shares process-global, mutable prediction + * state through the companion object — the decoded `ATN`, the `decisionToDFA` array, and the + * `PredictionContextCache`. `adaptivePredict` mutates those shared `DFA` structures (adding states/edges) with + * no locking, so compiling JSONPath expressions concurrently races on that shared state. On Kotlin/Native + * (strict, no forgiving JIT) this surfaces as thrown exceptions (NPE, index-out-of-bounds, illegal state) or, + * worse, silently wrong parses — and can even hard-crash the worker. + * + * These tests are therefore **expected to FAIL** against an unpatched antlr-kotlin: they assert that no worker + * threw and that every concurrent compile produced the same result as a single-threaded baseline. They exist to + * (a) demonstrate the defect and (b) measure how far a thread-safety patch to antlr-kotlin gets us. + * + * `TestScope` (virtual time) is disabled per test: matrix/coroutine real concurrency cannot run under virtual + * time, and we deliberately want real threads. + */ + +private const val WORKERS = 64 +private const val ITERATIONS = 250 +private const val ROUNDS = 8 + +private val realConcurrency = TestConfig.testScope(isEnabled = false) + +// Non-trivial, valid expressions. Filters, function extensions, logical operators, slices and descendant +// segments all force the parser through lots of ATN prediction / DFA construction — the racy hot path. +private val bookstoreExpressions = listOf( + "$.store.book[*].author", + "$..author", + "$.store.*", + "$.store..price", + "$..book[2]", + "$..book[-1]", + "$..book[0,1]", + "$..book[:2]", + "$..book[?@.isbn]", + "$..book[?@.price<10]", + "$..*", + "$.store.book[?@.price < 10 && @.category == 'fiction'].title", + "$.store.book[?match(@.category, 'fic.*')].author", + "$.store.book[?search(@.title, 'the')].title", +) + +// Expressions that only need to *compile* (type-checkable standalone) — used for the pure-compile race. +private val compileOnlyExpressions = bookstoreExpressions + listOf( + "$[?length(@) < 3]", + "$[?count(@.*) == 1]", + "$[?value(@..color) == 'red']", + "$[?match(@.timezone, 'Europe/.*')]", + "$[?@.a || @.b && @.c]", + "$[1:5:2]", + "$.o[?@<3, ?@<3]", +) + +private val bookStore: JsonElement = Json.decodeFromString( + """ + { "store": { + "book": [ + { "category": "reference", "author": "Nigel Rees", "title": "Sayings of the Century", "price": 8.95 }, + { "category": "fiction", "author": "Evelyn Waugh", "title": "Sword of Honour", "price": 12.99 }, + { "category": "fiction", "author": "Herman Melville", "title": "Moby Dick", "isbn": "0-553-21311-3", "price": 8.99 }, + { "category": "fiction", "author": "J. R. R. Tolkien", "title": "The Lord of the Rings", "isbn": "0-395-19395-8", "price": 22.99 } + ], + "bicycle": { "color": "red", "price": 399 } + } + } + """.trimIndent() +) + +private class WrongResult(message: String) : Exception(message) + +/** + * Runs [block] on [WORKERS] coroutines dispatched to a real, multi-threaded pool. All workers wait on a start + * gate so they hit the hot path simultaneously (maximizing the race window), then each runs [ITERATIONS] times. + * Each worker collects its own failures locally (no shared mutable state), aggregated after `awaitAll`. + */ +@OptIn(DelicateCoroutinesApi::class) +private suspend fun stress( + label: String, + dispatcher: CoroutineContext, + block: (worker: Int, iteration: Int) -> Unit, +): List = coroutineScope { + val gate = CompletableDeferred() + val deferred = (0 until WORKERS).map { worker -> + async(dispatcher) { + gate.await() + val local = ArrayList() + repeat(ITERATIONS) { iteration -> + try { + block(worker, iteration) + } catch (t: Throwable) { + local += t + } + } + local + } + } + gate.complete(Unit) // release all workers at once + deferred.awaitAll().flatten().also { failures -> + if (failures.isNotEmpty()) { + val summary = failures + .groupingBy { "${it::class.simpleName}: ${it.message}" } + .eachCount() + .entries + .sortedByDescending { it.value } + .joinToString("\n") { (kind, count) -> " x$count $kind" } + throw AssertionError( + "[$label] ${failures.size} of ${WORKERS * ITERATIONS} concurrent ANTLR operations failed " + + "(antlr-kotlin is not thread-safe). Distinct failures:\n$summary" + ) + } + } +} + +val AntlrConcurrencyNativeTest by testSuite { + + // 1) COLD DFA, SAME EXPRESSION: every worker compiles the identical complex expression with no warmup, so + // they all race to build the very same shared DFA decision states at once — the tightest contention. + test("concurrent compilation of the same expression on a cold parser", testConfig = realConcurrency) { + val pool = newFixedThreadPoolContext(WORKERS, "antlr-same") + try { + withTimeout(120.seconds) { + repeat(ROUNDS) { + val expr = "$.store.book[?@.price < 10 && @.category == 'fiction'].title" + stress("same-expr", pool) { _, _ -> + JsonPath(expr) + } + } + } + } finally { + pool.close() + } + } + + // 2) MANY DISTINCT EXPRESSIONS: workers pick different expressions, maximizing churn across many decisions + // and the shared PredictionContextCache simultaneously. + test("concurrent compilation of many distinct expressions", testConfig = realConcurrency) { + val pool = newFixedThreadPoolContext(WORKERS, "antlr-distinct") + try { + withTimeout(120.seconds) { + repeat(ROUNDS) { + stress("distinct-expr", pool) { worker, iteration -> + val expr = compileOnlyExpressions[(worker + iteration) % compileOnlyExpressions.size] + JsonPath(expr) + } + } + } + } finally { + pool.close() + } + } + + // 3) CORRECTNESS UNDER CONCURRENCY: compile + query in parallel and compare against a single-threaded + // baseline. Catches silent corruption (no exception, but a wrong/incomplete parse -> wrong node list). + test("concurrent compilation yields results consistent with a single-threaded baseline", testConfig = realConcurrency) { + // Baseline computed sequentially, before any concurrency touches the shared ANTLR state. + val baseline: Map = bookstoreExpressions.associateWith { expr -> + JsonPath(expr).query(bookStore).map { it.value }.toString() + } + val pool = newFixedThreadPoolContext(WORKERS, "antlr-correct") + try { + withTimeout(120.seconds) { + repeat(ROUNDS) { + stress("correctness", pool) { worker, iteration -> + val expr = bookstoreExpressions[(worker + iteration) % bookstoreExpressions.size] + val actual = JsonPath(expr).query(bookStore).map { it.value }.toString() + val expected = baseline.getValue(expr) + if (actual != expected) { + throw WrongResult("expr <$expr>: expected $expected but got $actual") + } + } + } + } + } finally { + pool.close() + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 584f24e..b5c3900 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,11 +2,13 @@ rootProject.name = "JsonPath4K" pluginManagement { repositories { - maven("https://s01.oss.sonatype.org/content/repositories/snapshots") //KOTEST snapshot - maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + maven { + url = uri("https://raw.githubusercontent.com/a-sit-plus/gradle-conventions-plugin/mvn/repo") + name = "aspConventions" + } google() - gradlePluginPortal() mavenCentral() + gradlePluginPortal() } } @@ -17,4 +19,4 @@ dependencyResolutionManagement { } } -include(":jsonpath4k") \ No newline at end of file +include(":jsonpath4k") From 24f37bddaf87949c83d319388f6dee305fb565e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Wed, 22 Jul 2026 18:16:04 +0200 Subject: [PATCH 02/10] concurrency --- .github/workflows/publish-pages-only.yml | 4 +- .github/workflows/publish.yml | 4 +- build.gradle.kts | 9 +++ gradle/libs.versions.toml | 4 +- gradle/wrapper/gradle-wrapper.properties | 2 +- jsonpath4k/build.gradle.kts | 14 +++- .../kotlin/at/asitplus/jsonpath/JsonPath.kt | 78 ++++++++++++++++++- 7 files changed, 102 insertions(+), 13 deletions(-) diff --git a/.github/workflows/publish-pages-only.yml b/.github/workflows/publish-pages-only.yml index 5095286..958b774 100644 --- a/.github/workflows/publish-pages-only.yml +++ b/.github/workflows/publish-pages-only.yml @@ -20,14 +20,14 @@ jobs: distribution: 'temurin' java-version: '17' - name: Build Dokka HTML - run: ./gradlew dokkaHtml + run: ./gradlew :jsonpath4k:dokkaGenerate - name: Setup Pages uses: actions/configure-pages@v3 - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: # Upload docs folder - path: './build/dokka' + path: './docs' - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 04613c0..a56d174 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -36,14 +36,14 @@ jobs: with: submodules: recursive - name: Build Dokka HTML - run: ./gradlew dokkaHtml + run: ./gradlew :jsonpath4k:dokkaGenerate - name: Setup Pages uses: actions/configure-pages@v3 - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: # Upload docs folder - path: './build/dokka' + path: './docs' - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 diff --git a/build.gradle.kts b/build.gradle.kts index 1c256d2..082ca18 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,5 @@ +import org.gradle.kotlin.dsl.support.listFilesOrdered + plugins { kotlin("multiplatform") version libs.versions.kotlin.get() apply false kotlin("plugin.serialization") version libs.versions.kotlin.get() apply false @@ -8,3 +10,10 @@ plugins { val artifactVersion: String by extra group = "at.asitplus" version = artifactVersion + +tasks.register("dokkaGenerateSite") { + dependsOn(":jsonpath4k:dokkaGeneratePublicationHtml") + from(project(":jsonpath4k").layout.buildDirectory.dir("dokka/html")) + from(rootDir.listFilesOrdered { it.extension.lowercase() == "png" || it.extension.lowercase() == "svg" }) + into(rootProject.layout.projectDirectory.dir("docs")) +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 772c0ba..d8dfe6a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] kotlin = "2.4.0" -asp-conventions = "20260701" -agp = "8.12.3" +asp-conventions = "20260722" +agp = "9.0.1" antlr-kotlin = "1.0.12" kotest = "5.9.1" testballoon = "1.0.1-K2.4.0" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index bad7c24..a351597 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/jsonpath4k/build.gradle.kts b/jsonpath4k/build.gradle.kts index 657a3b9..14e458e 100644 --- a/jsonpath4k/build.gradle.kts +++ b/jsonpath4k/build.gradle.kts @@ -100,7 +100,7 @@ kotlin { dependencies { implementation(libs.antlr.kotlin) implementation(serialization("json")) - implementation(napier()) + // implementation(napier()) } } commonTest { @@ -126,10 +126,20 @@ tasks.withType { exportXCFramework("JsonPath4K", transitiveExports = false) -val javadocJar = setupDokka( +setupDokka( baseUrl = "https://github.com/a-sit-plus/jsonpath4k/tree/main" ) +val moduleDesc = rootProject.file("dokka-tmp.md").apply { + writeText("# Module jsonpath4k\n${rootProject.file("README.md").readText().replaceFirst("# ", "")}") +} +dokka { + moduleName.set("jsonpath4k") + dokkaSourceSets.configureEach { + includes.from(moduleDesc) + } +} + val javadocRedirectJar = tasks.register("javadocRedirectJar") { archiveClassifier.set("javadoc") from(project.rootDir.absolutePath+"/javadoc") diff --git a/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/JsonPath.kt b/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/JsonPath.kt index 627c176..92931f9 100644 --- a/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/JsonPath.kt +++ b/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/JsonPath.kt @@ -12,7 +12,7 @@ import at.asitplus.jsonpath.core.functionExtensions.valueFunctionExtension import at.asitplus.jsonpath.implementation.AntlrJsonPathCompiler import at.asitplus.jsonpath.implementation.AntlrJsonPathCompilerErrorListener import com.strumenta.antlrkotlin.runtime.BitSet -import io.github.aakira.napier.Napier +//import io.github.aakira.napier.Napier import kotlinx.serialization.json.JsonElement import org.antlr.v4.kotlinruntime.Parser import org.antlr.v4.kotlinruntime.RecognitionException @@ -39,7 +39,76 @@ class JsonPath( * Default json path compiler used when no compiler is explicitly chosen */ var defaultCompiler: JsonPathCompiler = AntlrJsonPathCompiler( - errorListener = napierAntlrJsonPathCompilerErrorListener, + errorListener = object : AntlrJsonPathCompilerErrorListener { + override fun unknownFunctionExtension(functionExtensionName: String) { + // TODO("Not yet implemented") + } + + override fun invalidFunctionExtensionForTestExpression(functionExtensionName: String) { + // TODO("Not yet implemented") + } + + override fun invalidFunctionExtensionForComparable(functionExtensionName: String) { + //TODO("Not yet implemented") + } + + override fun invalidArglistForFunctionExtension( + functionExtensionName: String, + functionExtensionImplementation: JsonPathFunctionExtension, + coercedArgumentTypes: List> + ) { + // TODO("Not yet implemented") + } + + override fun invalidTestExpression(testContextString: String) { + // TODO("Not yet implemented") + } + + override fun syntaxError( + recognizer: Recognizer<*, *>, + offendingSymbol: Any?, + line: Int, + charPositionInLine: Int, + msg: String, + e: RecognitionException? + ) { + //TODO("Not yet implemented") + } + + override fun reportAmbiguity( + recognizer: Parser, + dfa: DFA, + startIndex: Int, + stopIndex: Int, + exact: Boolean, + ambigAlts: BitSet, + configs: ATNConfigSet + ) { + // TODO("Not yet implemented") + } + + override fun reportAttemptingFullContext( + recognizer: Parser, + dfa: DFA, + startIndex: Int, + stopIndex: Int, + conflictingAlts: BitSet, + configs: ATNConfigSet + ) { + // TODO("Not yet implemented") + } + + override fun reportContextSensitivity( + recognizer: Parser, + dfa: DFA, + startIndex: Int, + stopIndex: Int, + prediction: Int, + configs: ATNConfigSet + ) { + // TODO("Not yet implemented") + } + }, ) /** @@ -56,7 +125,7 @@ class JsonPath( ) } } - +/* private val napierAntlrJsonPathCompilerErrorListener by lazy { object : AntlrJsonPathCompilerErrorListener { override fun unknownFunctionExtension(functionExtensionName: String) { @@ -146,4 +215,5 @@ private val napierAntlrJsonPathCompilerErrorListener by lazy { } } } -} \ No newline at end of file +} +*/ \ No newline at end of file From a8af70e8d9b5b815fbc4b88ade81ab4ea22bc1d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Wed, 22 Jul 2026 19:27:37 +0200 Subject: [PATCH 03/10] all targets + proper testsing --- CHANGELOG.md | 12 +- gradle/libs.versions.toml | 1 - jsonpath4k/build.gradle.kts | 26 +- .../asitplus/jsonpath/JsonPath.androidJvm.kt | 101 ++++++++ .../jsonpath/JsonPath.androidNative.kt | 3 + .../kotlin/at/asitplus/jsonpath/JsonPath.kt | 238 +++++------------- .../jsonpath/JsonPathDependencyManager.kt | 26 -- .../AntlrJsonPathSemanticAnalyzerVisitor.kt | 2 +- .../src/commonTest/kotlin/TestConfig.kt | 13 + .../jsonpath/DependencyManagementTest.kt | 2 + .../at/asitplus/jsonpath/JsonPathUnitTest.kt | 4 +- .../MemberNameShorthandSerializationTest.kt | 50 ++-- .../NormalizedJsonPathSerializationTest.kt | 77 +++--- .../asitplus/jsonpath/Rfc9535UtilsUnitTest.kt | 139 +++++----- .../at/asitplus/jsonpath/JsonPath.ios.kt | 101 ++++++++ .../at/asitplus/jsonpath/JsonPath.linux.kt | 3 + .../at/asitplus/jsonpath/JsonPath.macos.kt | 101 ++++++++ .../at/asitplus/jsonpath/JsonPath.mingw.kt | 3 + .../jsonpath/AntlrConcurrencyNativeTest.kt | 195 ++++---------- .../AntlrCoroutineConcurrencyNativeTest.kt | 115 +++++++++ .../jsonpath/ConcurrencyStressFixtures.kt | 78 ++++++ .../at/asitplus/jsonpath/JsonPath.tvos.kt | 101 ++++++++ .../at/asitplus/jsonpath/JsonPath.watchos.kt | 3 + .../at/asitplus/jsonpath/JsonPath.web.kt | 103 ++++++++ 24 files changed, 1018 insertions(+), 479 deletions(-) create mode 100644 jsonpath4k/src/androidJvmMain/kotlin/at/asitplus/jsonpath/JsonPath.androidJvm.kt create mode 100644 jsonpath4k/src/androidNativeMain/kotlin/at/asitplus/jsonpath/JsonPath.androidNative.kt delete mode 100644 jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/JsonPathDependencyManager.kt create mode 100644 jsonpath4k/src/commonTest/kotlin/TestConfig.kt create mode 100644 jsonpath4k/src/iosMain/kotlin/at/asitplus/jsonpath/JsonPath.ios.kt create mode 100644 jsonpath4k/src/linuxMain/kotlin/at/asitplus/jsonpath/JsonPath.linux.kt create mode 100644 jsonpath4k/src/macosMain/kotlin/at/asitplus/jsonpath/JsonPath.macos.kt create mode 100644 jsonpath4k/src/mingwMain/kotlin/at/asitplus/jsonpath/JsonPath.mingw.kt create mode 100644 jsonpath4k/src/nativeTest/kotlin/at/asitplus/jsonpath/AntlrCoroutineConcurrencyNativeTest.kt create mode 100644 jsonpath4k/src/nativeTest/kotlin/at/asitplus/jsonpath/ConcurrencyStressFixtures.kt create mode 100644 jsonpath4k/src/tvosMain/kotlin/at/asitplus/jsonpath/JsonPath.tvos.kt create mode 100644 jsonpath4k/src/watchosMain/kotlin/at/asitplus/jsonpath/JsonPath.watchos.kt create mode 100644 jsonpath4k/src/webMain/kotlin/at/asitplus/jsonpath/JsonPath.web.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 1946073..24fe3a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,17 @@ # CHANGELOG # Release 3.2.0-SNAPSHOT - * TODO +* Support all KMP targets + * Logging through Napier is only supported on + * Android (ART) + * JVM + * iOS + * tvOS + * macOS + * JVM + * Logging on other targets defaults to NOOP +* Kotlin 2.4.0 +* Fix race condition thanks to latest ANTLR 1.0.13 # Release 3.1.0 * Add: structural equality check for `NormalizedJsonPath` and `NormalizedJsonPathSegment` diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d8dfe6a..d293079 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,6 @@ kotlin = "2.4.0" asp-conventions = "20260722" agp = "9.0.1" antlr-kotlin = "1.0.12" -kotest = "5.9.1" testballoon = "1.0.1-K2.4.0" [libraries] diff --git a/jsonpath4k/build.gradle.kts b/jsonpath4k/build.gradle.kts index 14e458e..12a6002 100644 --- a/jsonpath4k/build.gradle.kts +++ b/jsonpath4k/build.gradle.kts @@ -1,14 +1,16 @@ import at.asitplus.gradle.Logger +import at.asitplus.gradle.androidJvmMain import at.asitplus.gradle.coroutines import at.asitplus.gradle.exportXCFramework import at.asitplus.gradle.napier import at.asitplus.gradle.serialization import at.asitplus.gradle.setupDokka +import at.asitplus.gradle.tbAddons import com.strumenta.antlrkotlin.gradle.AntlrKotlinTask +import org.gradle.kotlin.dsl.dokka import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.plugin.extraProperties -import java.util.Properties -import kotlin.apply +import java.util.* plugins { alias(libs.plugins.android.kmp.library) @@ -100,7 +102,6 @@ kotlin { dependencies { implementation(libs.antlr.kotlin) implementation(serialization("json")) - // implementation(napier()) } } commonTest { @@ -110,8 +111,23 @@ kotlin { implementation(serialization("json")) // Needed by the highly-concurrent native ANTLR stress test in nativeTest. implementation(coroutines()) + implementation(tbAddons("matrix")) } } + androidJvmMain { + dependencies { implementation(napier()) } + } + iosMain.dependencies { + implementation(napier()) + } + webMain.dependencies { + implementation(napier()) + } + macosMain.dependencies { + implementation(napier()) + } + tvosMain.dependencies {implementation(napier())} + } } @@ -131,7 +147,7 @@ setupDokka( ) val moduleDesc = rootProject.file("dokka-tmp.md").apply { - writeText("# Module jsonpath4k\n${rootProject.file("README.md").readText().replaceFirst("# ", "")}") + writeText("# Module \n${rootProject.file("README.md").readText().replaceFirst("# ", "")}") } dokka { moduleName.set("jsonpath4k") @@ -142,7 +158,7 @@ dokka { val javadocRedirectJar = tasks.register("javadocRedirectJar") { archiveClassifier.set("javadoc") - from(project.rootDir.absolutePath+"/javadoc") + from(project.rootDir.absolutePath + "/javadoc") } publishing { diff --git a/jsonpath4k/src/androidJvmMain/kotlin/at/asitplus/jsonpath/JsonPath.androidJvm.kt b/jsonpath4k/src/androidJvmMain/kotlin/at/asitplus/jsonpath/JsonPath.androidJvm.kt new file mode 100644 index 0000000..7f6eb54 --- /dev/null +++ b/jsonpath4k/src/androidJvmMain/kotlin/at/asitplus/jsonpath/JsonPath.androidJvm.kt @@ -0,0 +1,101 @@ +package at.asitplus.jsonpath + +import at.asitplus.jsonpath.core.JsonPathFilterExpressionType +import at.asitplus.jsonpath.core.JsonPathFunctionExtension +import at.asitplus.jsonpath.implementation.AntlrJsonPathCompilerErrorListener +import com.strumenta.antlrkotlin.runtime.BitSet +import io.github.aakira.napier.Napier +import org.antlr.v4.kotlinruntime.Parser +import org.antlr.v4.kotlinruntime.RecognitionException +import org.antlr.v4.kotlinruntime.Recognizer +import org.antlr.v4.kotlinruntime.atn.ATNConfigSet +import org.antlr.v4.kotlinruntime.dfa.DFA + +internal actual val defaultErrorListener = object : AntlrJsonPathCompilerErrorListener { + override fun unknownFunctionExtension(functionExtensionName: String) { + Napier.e { + "Unknown JSONPath function extension: \"$functionExtensionName\"" + } + } + + override fun invalidFunctionExtensionForTestExpression(functionExtensionName: String) { + Napier.e { + "Invalid JSONPath function extension return type for test expression: \"$functionExtensionName\"" + } + } + + override fun invalidFunctionExtensionForComparable(functionExtensionName: String) { + Napier.e { + "Invalid JSONPath function extension return type for comparable expression: \"$functionExtensionName\"" + } + } + + override fun invalidArglistForFunctionExtension( + functionExtensionName: String, + functionExtensionImplementation: JsonPathFunctionExtension, + coercedArgumentTypes: List> + ) { + Napier.e { + "Invalid arguments for function extension \"$functionExtensionName\": Expected: <${ + functionExtensionImplementation.argumentTypes.joinToString( + ", " + ) + }>, but received <${ + coercedArgumentTypes.map { it.first }.joinToString(", ") + }>: <${coercedArgumentTypes.map { it.second }.joinToString(", ")}>" + } + } + + override fun invalidTestExpression(testContextString: String) { + Napier.e { + "Invalid test expression: $testContextString" + } + } + + override fun reportAmbiguity( + recognizer: Parser, + dfa: DFA, + startIndex: Int, + stopIndex: Int, + exact: Boolean, + ambigAlts: BitSet, + configs: ATNConfigSet + ) { + // noop + } + + override fun reportAttemptingFullContext( + recognizer: Parser, + dfa: DFA, + startIndex: Int, + stopIndex: Int, + conflictingAlts: BitSet, + configs: ATNConfigSet + ) { + // noop + } + + override fun reportContextSensitivity( + recognizer: Parser, + dfa: DFA, + startIndex: Int, + stopIndex: Int, + prediction: Int, + configs: ATNConfigSet + ) { + // noop + } + + override fun syntaxError( + recognizer: Recognizer<*, *>, + offendingSymbol: Any?, + line: Int, + charPositionInLine: Int, + msg: String, + e: RecognitionException? + ) { + Napier.e { + "Syntax error $line:$charPositionInLine $msg" + } + } +} \ No newline at end of file diff --git a/jsonpath4k/src/androidNativeMain/kotlin/at/asitplus/jsonpath/JsonPath.androidNative.kt b/jsonpath4k/src/androidNativeMain/kotlin/at/asitplus/jsonpath/JsonPath.androidNative.kt new file mode 100644 index 0000000..aa8bf94 --- /dev/null +++ b/jsonpath4k/src/androidNativeMain/kotlin/at/asitplus/jsonpath/JsonPath.androidNative.kt @@ -0,0 +1,3 @@ +package at.asitplus.jsonpath + +internal actual val defaultErrorListener = noopErrorListener \ No newline at end of file diff --git a/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/JsonPath.kt b/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/JsonPath.kt index 92931f9..42d6d12 100644 --- a/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/JsonPath.kt +++ b/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/JsonPath.kt @@ -1,18 +1,14 @@ package at.asitplus.jsonpath +//import io.github.aakira.napier.Napier import at.asitplus.jsonpath.core.JsonPathCompiler import at.asitplus.jsonpath.core.JsonPathFilterExpressionType import at.asitplus.jsonpath.core.JsonPathFunctionExtension import at.asitplus.jsonpath.core.NodeList -import at.asitplus.jsonpath.core.functionExtensions.countFunctionExtension -import at.asitplus.jsonpath.core.functionExtensions.lengthFunctionExtension -import at.asitplus.jsonpath.core.functionExtensions.matchFunctionExtension -import at.asitplus.jsonpath.core.functionExtensions.searchFunctionExtension -import at.asitplus.jsonpath.core.functionExtensions.valueFunctionExtension +import at.asitplus.jsonpath.core.functionExtensions.* import at.asitplus.jsonpath.implementation.AntlrJsonPathCompiler import at.asitplus.jsonpath.implementation.AntlrJsonPathCompilerErrorListener import com.strumenta.antlrkotlin.runtime.BitSet -//import io.github.aakira.napier.Napier import kotlinx.serialization.json.JsonElement import org.antlr.v4.kotlinruntime.Parser import org.antlr.v4.kotlinruntime.RecognitionException @@ -38,182 +34,84 @@ class JsonPath( /** * Default json path compiler used when no compiler is explicitly chosen */ - var defaultCompiler: JsonPathCompiler = AntlrJsonPathCompiler( - errorListener = object : AntlrJsonPathCompilerErrorListener { - override fun unknownFunctionExtension(functionExtensionName: String) { - // TODO("Not yet implemented") - } - - override fun invalidFunctionExtensionForTestExpression(functionExtensionName: String) { - // TODO("Not yet implemented") - } - - override fun invalidFunctionExtensionForComparable(functionExtensionName: String) { - //TODO("Not yet implemented") - } - - override fun invalidArglistForFunctionExtension( - functionExtensionName: String, - functionExtensionImplementation: JsonPathFunctionExtension, - coercedArgumentTypes: List> - ) { - // TODO("Not yet implemented") - } - - override fun invalidTestExpression(testContextString: String) { - // TODO("Not yet implemented") - } - - override fun syntaxError( - recognizer: Recognizer<*, *>, - offendingSymbol: Any?, - line: Int, - charPositionInLine: Int, - msg: String, - e: RecognitionException? - ) { - //TODO("Not yet implemented") - } - - override fun reportAmbiguity( - recognizer: Parser, - dfa: DFA, - startIndex: Int, - stopIndex: Int, - exact: Boolean, - ambigAlts: BitSet, - configs: ATNConfigSet - ) { - // TODO("Not yet implemented") - } - - override fun reportAttemptingFullContext( - recognizer: Parser, - dfa: DFA, - startIndex: Int, - stopIndex: Int, - conflictingAlts: BitSet, - configs: ATNConfigSet - ) { - // TODO("Not yet implemented") - } - - override fun reportContextSensitivity( - recognizer: Parser, - dfa: DFA, - startIndex: Int, - stopIndex: Int, - prediction: Int, - configs: ATNConfigSet - ) { - // TODO("Not yet implemented") - } - }, - ) + var defaultCompiler: JsonPathCompiler = AntlrJsonPathCompiler(defaultErrorListener) /** * Function extension repository that may be extended with custom functions by the user of this library. */ - var defaultFunctionExtensionRepository: JsonPathFunctionExtensionRepository = JsonPathFunctionExtensionMapRepository( - listOf( - lengthFunctionExtension, - countFunctionExtension, - matchFunctionExtension, - searchFunctionExtension, - valueFunctionExtension, - ).toMap().toMutableMap() - ) + var defaultFunctionExtensionRepository: JsonPathFunctionExtensionRepository = + JsonPathFunctionExtensionMapRepository( + listOf( + lengthFunctionExtension, + countFunctionExtension, + matchFunctionExtension, + searchFunctionExtension, + valueFunctionExtension, + ).toMap().toMutableMap() + ) } } -/* -private val napierAntlrJsonPathCompilerErrorListener by lazy { - object : AntlrJsonPathCompilerErrorListener { - override fun unknownFunctionExtension(functionExtensionName: String) { - Napier.e { - "Unknown JSONPath function extension: \"$functionExtensionName\"" - } - } - override fun invalidFunctionExtensionForTestExpression(functionExtensionName: String) { - Napier.e { - "Invalid JSONPath function extension return type for test expression: \"$functionExtensionName\"" - } - } +internal expect val defaultErrorListener: AntlrJsonPathCompilerErrorListener - override fun invalidFunctionExtensionForComparable(functionExtensionName: String) { - Napier.e { - "Invalid JSONPath function extension return type for comparable expression: \"$functionExtensionName\"" - } - } +internal val noopErrorListener = object : AntlrJsonPathCompilerErrorListener { + override fun unknownFunctionExtension(functionExtensionName: String) { + } - override fun invalidArglistForFunctionExtension( - functionExtensionName: String, - functionExtensionImplementation: JsonPathFunctionExtension, - coercedArgumentTypes: List> - ) { - Napier.e { - "Invalid arguments for function extension \"$functionExtensionName\": Expected: <${ - functionExtensionImplementation.argumentTypes.joinToString( - ", " - ) - }>, but received <${ - coercedArgumentTypes.map { it.first }.joinToString(", ") - }>: <${coercedArgumentTypes.map { it.second }.joinToString(", ")}>" - } - } + override fun invalidFunctionExtensionForTestExpression(functionExtensionName: String) { + } + + override fun invalidFunctionExtensionForComparable(functionExtensionName: String) { + } + + override fun invalidArglistForFunctionExtension( + functionExtensionName: String, + functionExtensionImplementation: JsonPathFunctionExtension, + coercedArgumentTypes: List> + ) { + } - override fun invalidTestExpression(testContextString: String) { - Napier.e { - "Invalid test expression: $testContextString" - } - } + override fun invalidTestExpression(testContextString: String) { + } - override fun reportAmbiguity( - recognizer: Parser, - dfa: DFA, - startIndex: Int, - stopIndex: Int, - exact: Boolean, - ambigAlts: BitSet, - configs: ATNConfigSet - ) { - // noop - } + override fun syntaxError( + recognizer: Recognizer<*, *>, + offendingSymbol: Any?, + line: Int, + charPositionInLine: Int, + msg: String, + e: RecognitionException? + ) { + } - override fun reportAttemptingFullContext( - recognizer: Parser, - dfa: DFA, - startIndex: Int, - stopIndex: Int, - conflictingAlts: BitSet, - configs: ATNConfigSet - ) { - // noop - } + override fun reportAmbiguity( + recognizer: Parser, + dfa: DFA, + startIndex: Int, + stopIndex: Int, + exact: Boolean, + ambigAlts: BitSet, + configs: ATNConfigSet + ) { + } - override fun reportContextSensitivity( - recognizer: Parser, - dfa: DFA, - startIndex: Int, - stopIndex: Int, - prediction: Int, - configs: ATNConfigSet - ) { - // noop - } + override fun reportAttemptingFullContext( + recognizer: Parser, + dfa: DFA, + startIndex: Int, + stopIndex: Int, + conflictingAlts: BitSet, + configs: ATNConfigSet + ) { + } - override fun syntaxError( - recognizer: Recognizer<*, *>, - offendingSymbol: Any?, - line: Int, - charPositionInLine: Int, - msg: String, - e: RecognitionException? - ) { - Napier.e { - "Syntax error $line:$charPositionInLine $msg" - } - } + override fun reportContextSensitivity( + recognizer: Parser, + dfa: DFA, + startIndex: Int, + stopIndex: Int, + prediction: Int, + configs: ATNConfigSet + ) { } -} -*/ \ No newline at end of file +} \ No newline at end of file diff --git a/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/JsonPathDependencyManager.kt b/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/JsonPathDependencyManager.kt deleted file mode 100644 index 376e047..0000000 --- a/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/JsonPathDependencyManager.kt +++ /dev/null @@ -1,26 +0,0 @@ -package at.asitplus.jsonpath - -import at.asitplus.jsonpath.core.JsonPathCompiler - -@Deprecated("Dependencies have been moved to JsonPath.Companion") -object JsonPathDependencyManager { - @Deprecated( - "To be removed in version 4. Dependencies have been moved to JsonPath.Companion", - ReplaceWith("JsonPath.defaultFunctionExtensionRepository"), - ) - var functionExtensionRepository: JsonPathFunctionExtensionRepository - get() = JsonPath.defaultFunctionExtensionRepository - set(value) { - JsonPath.defaultFunctionExtensionRepository = value - } - - @Deprecated( - "To be removed in version 4. Dependencies have been moved to JsonPath.Companion", - ReplaceWith("JsonPath.defaultCompiler"), - ) - var compiler: JsonPathCompiler - get() = JsonPath.defaultCompiler - set(value) { - JsonPath.defaultCompiler = value - } -} \ No newline at end of file diff --git a/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/implementation/AntlrJsonPathSemanticAnalyzerVisitor.kt b/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/implementation/AntlrJsonPathSemanticAnalyzerVisitor.kt index 6eadbf8..c35823e 100644 --- a/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/implementation/AntlrJsonPathSemanticAnalyzerVisitor.kt +++ b/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/implementation/AntlrJsonPathSemanticAnalyzerVisitor.kt @@ -25,7 +25,7 @@ import org.antlr.v4.kotlinruntime.tree.TerminalNode * This class builds an abstract syntax tree where the nodes contain the logic necessary to be evaluated against an input. */ internal class AntlrJsonPathSemanticAnalyzerVisitor( - private val errorListener: AntlrJsonPathSemanticAnalyzerErrorListener?, + private val errorListener: at.asitplus.jsonpath.implementation.AntlrJsonPathSemanticAnalyzerErrorListener?, private val functionExtensionRetriever: (String) -> JsonPathFunctionExtension?, ) : JsonPathParserBaseVisitor>() { override fun defaultResult(): AbstractSyntaxTree { diff --git a/jsonpath4k/src/commonTest/kotlin/TestConfig.kt b/jsonpath4k/src/commonTest/kotlin/TestConfig.kt new file mode 100644 index 0000000..07db777 --- /dev/null +++ b/jsonpath4k/src/commonTest/kotlin/TestConfig.kt @@ -0,0 +1,13 @@ +import at.asitplus.testballoon.matrix.ExecutionMode +import at.asitplus.testballoon.matrix.MatrixTestDefaults +import de.infix.testBalloon.framework.core.TestSession + +/** + * Single [TestSession] for this module. The RFC-conformance suites mutate process-global state + * (`JsonPath.defaultCompiler`, `JsonPath.defaultFunctionExtensionRepository`), so the default matrix + * execution is [ExecutionMode.Sequential]. The dedicated ANTLR concurrency stress suite opts into real + * parallelism explicitly (see the nativeTest source set). + */ +class TestConfig : TestSession( + testConfig = DefaultConfiguration.apply { MatrixTestDefaults { execution = ExecutionMode.Concurrent(24) } } +) \ No newline at end of file diff --git a/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/DependencyManagementTest.kt b/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/DependencyManagementTest.kt index dc84865..baa50f9 100644 --- a/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/DependencyManagementTest.kt +++ b/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/DependencyManagementTest.kt @@ -3,6 +3,7 @@ package at.asitplus.jsonpath import at.asitplus.jsonpath.core.JsonPathCompiler import at.asitplus.jsonpath.core.JsonPathFunctionExtension import at.asitplus.jsonpath.core.JsonPathQuery +import at.asitplus.testballoon.matrix.ExecutionMode import at.asitplus.testballoon.matrix.matrixConfig import at.asitplus.testballoon.matrix.matrixSuite import de.infix.testBalloon.framework.core.TestConfig @@ -21,6 +22,7 @@ private val defaultFunctionExtensionRepositoryBackup = val DependencyManagementTest by matrixSuite( matrixConfig { + execution = ExecutionMode.Sequential // Reset the global dependencies to their defaults around every test (was beforeEach/afterEach). testConfig = TestConfig.aroundEachTest { action -> // prepare a dummy repository to be modified by the tests diff --git a/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/JsonPathUnitTest.kt b/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/JsonPathUnitTest.kt index 296370c..9d1e611 100644 --- a/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/JsonPathUnitTest.kt +++ b/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/JsonPathUnitTest.kt @@ -4,7 +4,9 @@ import at.asitplus.jsonpath.core.JsonPathFilterExpressionType import at.asitplus.jsonpath.core.JsonPathFunctionExtension import at.asitplus.jsonpath.implementation.JsonPathParserException import at.asitplus.jsonpath.implementation.JsonPathTypeCheckerException +import at.asitplus.testballoon.matrix.ExecutionMode import at.asitplus.testballoon.matrix.MatrixSuiteScope +import at.asitplus.testballoon.matrix.matrixConfig import at.asitplus.testballoon.matrix.matrixSuite import de.infix.testBalloon.framework.core.TestConfig import de.infix.testBalloon.framework.core.aroundEachTest @@ -51,7 +53,7 @@ private val defaultCompilerBuilderBackup = JsonPath.defaultCompiler private val defaultFunctionExtensionRepositoryBackup = JsonPath.defaultFunctionExtensionRepository.export() -val JsonPathUnitTest by matrixSuite { +val JsonPathUnitTest by matrixSuite(matrixConfig { execution= ExecutionMode.Sequential }) { "Examples from https://datatracker.ietf.org/doc/rfc9535/" - { "1.5. JSONPath Examples" - { val bookStore = Json.decodeFromString( diff --git a/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/MemberNameShorthandSerializationTest.kt b/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/MemberNameShorthandSerializationTest.kt index 4ee5176..8d2c3fc 100644 --- a/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/MemberNameShorthandSerializationTest.kt +++ b/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/MemberNameShorthandSerializationTest.kt @@ -7,32 +7,32 @@ import io.kotest.assertions.throwables.shouldThrowAny import io.kotest.matchers.shouldBe val MemberNameShorthandSerializationTest by matrixSuite { - "should be serializable as member name shorthand" - { - data( - listOf( - "test", - "test_123", - "t1", - ) - ) test { - shouldNotThrowAny { - NormalizedJsonPathSegment.NameSegment(it).toShorthandNotation() - } shouldBe ".$it" - } + data( + "should be serializable as member name shorthand", + listOf( + "test", + "test_123", + "t1", + ) + ) test { + shouldNotThrowAny { + NormalizedJsonPathSegment.NameSegment(it).toShorthandNotation() + } shouldBe ".$it" } - "should not be serializable as member name shorthand" - { - data( - listOf( - "1", - "*", - "'", - "\"", - "test-data", - ) - ) test { - shouldThrowAny { - NormalizedJsonPathSegment.NameSegment(it).toShorthandNotation() shouldBe ".$it" - } + + data( + "should not be serializable as member name shorthand", + listOf( + "1", + "*", + "'", + "\"", + "test-data", + ) + ) test { + shouldThrowAny { + NormalizedJsonPathSegment.NameSegment(it).toShorthandNotation() shouldBe ".$it" } } } + diff --git a/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/NormalizedJsonPathSerializationTest.kt b/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/NormalizedJsonPathSerializationTest.kt index abd4c11..586471f 100644 --- a/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/NormalizedJsonPathSerializationTest.kt +++ b/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/NormalizedJsonPathSerializationTest.kt @@ -8,52 +8,51 @@ import io.kotest.assertions.throwables.shouldThrowAny import io.kotest.matchers.shouldBe val NormalizedJsonPathSerializationTest by matrixSuite { - "normalized json path string" - { + data( + "normalized json path string", + listOf( + "test" to "$['test']", + "test_123" to "$['test_123']", + "t1" to "$['t1']", + ) + ) test { (input, expected) -> + shouldNotThrowAny { + val path = NormalizedJsonPath() + NormalizedJsonPathSegment.NameSegment(input) + path.toNormalizedJsonPathString() shouldBe expected + } + } + + "shorthand serialization" - { data( + "should be serializable as member name shorthand", listOf( - "test" to "$['test']", - "test_123" to "$['test_123']", - "t1" to "$['t1']", + "test", + "test_123", + "t1", ) - ) test { (input, expected) -> + ) test { shouldNotThrowAny { - val path = NormalizedJsonPath() + NormalizedJsonPathSegment.NameSegment(input) - path.toNormalizedJsonPathString() shouldBe expected - } - } - } - "shorthand serialization" - { - "should be serializable as member name shorthand" - { - data( - listOf( - "test", - "test_123", - "t1", - ) - ) test { - shouldNotThrowAny { - val path = NormalizedJsonPath() + NormalizedJsonPathSegment.NameSegment(it) - path.toShorthandNameSegmentNotation() shouldBe "$.$it" - path.toShorthandNameSegmentNotationWherePossible() shouldBe "$.$it" - } + val path = NormalizedJsonPath() + NormalizedJsonPathSegment.NameSegment(it) + path.toShorthandNameSegmentNotation() shouldBe "$.$it" + path.toShorthandNameSegmentNotationWherePossible() shouldBe "$.$it" } } - "should not be serializable as member name shorthand" - { - data( - listOf( - "1", - "*", - "'", - "\"", - "test-data", - ) - ) test { - val path = NormalizedJsonPath() + NormalizedJsonPathSegment.NameSegment(it) - shouldThrowAny { - path.toShorthandNameSegmentNotation() - } - path.toShorthandNameSegmentNotationWherePossible() shouldBe path.toNormalizedJsonPathString() + + data( + listOf( + "should not be serializable as member name shorthand", + "1", + "*", + "'", + "\"", + "test-data", + ) + ) test { + val path = NormalizedJsonPath() + NormalizedJsonPathSegment.NameSegment(it) + shouldThrowAny { + path.toShorthandNameSegmentNotation() } + path.toShorthandNameSegmentNotationWherePossible() shouldBe path.toNormalizedJsonPathString() } } } diff --git a/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/Rfc9535UtilsUnitTest.kt b/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/Rfc9535UtilsUnitTest.kt index 03a77e7..5f962c5 100644 --- a/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/Rfc9535UtilsUnitTest.kt +++ b/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/Rfc9535UtilsUnitTest.kt @@ -6,91 +6,90 @@ import io.kotest.matchers.shouldBe val Rfc9535UtilsUnitTest by matrixSuite { "Rfc9535Utils.unpackStringLiteral Unit Tests" - { - "rfc8259 conformance" - { - "special escape characters" - { - "double quoted" - { - data( - listOf( - "\"\\\\\"" to "\\", - "\"\\/\"" to "/", - "\"\\\"\"" to "\"", - "\"\\b\"" to Char(0x0008).toString(), - "\"\\f\"" to Char(0x000C).toString(), - "\"\\n\"" to "\n", - "\"\\r\"" to "\r", - "\"\\t\"" to "\t", - ), - nameFn = { (input, _) -> input }, - ) test { (input, expected) -> - Rfc9535Utils.unpackStringLiteral(input) shouldBe expected - } - } - "single quoted" - { - data( - listOf( - "'\\\\'" to "\\", - "'\\/'" to "/", - "'\\''" to "'", - "'\\b'" to Char(0x0008).toString(), - "'\\f'" to Char(0x000C).toString(), - "'\\n'" to "\n", - "'\\r'" to "\r", - "'\\t'" to "\t", - ), - nameFn = { (input, _) -> input }, - ) test { (input, expected) -> - Rfc9535Utils.unpackStringLiteral(input) shouldBe expected - } - } - } - } - } - "Rfc9535Utils.switchToSingleQuotedString Unit Tests" - { "rfc8259 conformance" - { "special escape characters" - { data( + "double quoted", listOf( - "\"\\\\\"" to "'\\\\'", - "\"\\/\"" to "'\\/'", - "\"\\\"\"" to "'\"'", - "\"'\"" to "'\\''", - "\"\\b\"" to "'\\b'", - "\"\\f\"" to "'\\f'", - "\"\\n\"" to "'\\n'", - "\"\\r\"" to "'\\r'", - "\"\\t\"" to "'\\t'", + "\"\\\\\"" to "\\", + "\"\\/\"" to "/", + "\"\\\"\"" to "\"", + "\"\\b\"" to Char(0x0008).toString(), + "\"\\f\"" to Char(0x000C).toString(), + "\"\\n\"" to "\n", + "\"\\r\"" to "\r", + "\"\\t\"" to "\t", ), nameFn = { (input, _) -> input }, ) test { (input, expected) -> - Rfc9535Utils.switchToSingleQuotedString(input) shouldBe expected - // switching an already single-quoted string is idempotent - Rfc9535Utils.switchToSingleQuotedString(expected) shouldBe expected + Rfc9535Utils.unpackStringLiteral(input) shouldBe expected } - } - } - } - "Rfc9535Utils.switchToDoubleQuotedString Unit Tests" - { - "rfc8259 conformance" - { - "special escape characters" - { + data( + "single quoted", listOf( - "'\\\\'" to "\"\\\\\"", - "'\\/'" to "\"\\/\"", - "'\"'" to "\"\\\"\"", - "'\\''" to "\"'\"", - "'\\b'" to "\"\\b\"", - "'\\f'" to "\"\\f\"", - "'\\n'" to "\"\\n\"", - "'\\r'" to "\"\\r\"", - "'\\t'" to "\"\\t\"", + "'\\\\'" to "\\", + "'\\/'" to "/", + "'\\''" to "'", + "'\\b'" to Char(0x0008).toString(), + "'\\f'" to Char(0x000C).toString(), + "'\\n'" to "\n", + "'\\r'" to "\r", + "'\\t'" to "\t", ), nameFn = { (input, _) -> input }, ) test { (input, expected) -> - Rfc9535Utils.switchToDoubleQuotedString(input) shouldBe expected - // switching an already double-quoted string is idempotent - Rfc9535Utils.switchToDoubleQuotedString(expected) shouldBe expected + Rfc9535Utils.unpackStringLiteral(input) shouldBe expected } } } } + + "Rfc9535Utils.switchToSingleQuotedString Unit Tests" - { + "rfc8259 conformance" - { + data( + "special escape characters", + listOf( + "\"\\\\\"" to "'\\\\'", + "\"\\/\"" to "'\\/'", + "\"\\\"\"" to "'\"'", + "\"'\"" to "'\\''", + "\"\\b\"" to "'\\b'", + "\"\\f\"" to "'\\f'", + "\"\\n\"" to "'\\n'", + "\"\\r\"" to "'\\r'", + "\"\\t\"" to "'\\t'", + ), + nameFn = { (input, _) -> input }, + ) test { (input, expected) -> + Rfc9535Utils.switchToSingleQuotedString(input) shouldBe expected + // switching an already single-quoted string is idempotent + Rfc9535Utils.switchToSingleQuotedString(expected) shouldBe expected + } + } + } + + "Rfc9535Utils.switchToDoubleQuotedString Unit Tests" - { + "rfc8259 conformance" - { + data( + "special escape characters", + listOf( + "'\\\\'" to "\"\\\\\"", + "'\\/'" to "\"\\/\"", + "'\"'" to "\"\\\"\"", + "'\\''" to "\"'\"", + "'\\b'" to "\"\\b\"", + "'\\f'" to "\"\\f\"", + "'\\n'" to "\"\\n\"", + "'\\r'" to "\"\\r\"", + "'\\t'" to "\"\\t\"", + ), + nameFn = { (input, _) -> input }, + ) test { (input, expected) -> + Rfc9535Utils.switchToDoubleQuotedString(input) shouldBe expected + // switching an already double-quoted string is idempotent + Rfc9535Utils.switchToDoubleQuotedString(expected) shouldBe expected + } + } + } } diff --git a/jsonpath4k/src/iosMain/kotlin/at/asitplus/jsonpath/JsonPath.ios.kt b/jsonpath4k/src/iosMain/kotlin/at/asitplus/jsonpath/JsonPath.ios.kt new file mode 100644 index 0000000..7f6eb54 --- /dev/null +++ b/jsonpath4k/src/iosMain/kotlin/at/asitplus/jsonpath/JsonPath.ios.kt @@ -0,0 +1,101 @@ +package at.asitplus.jsonpath + +import at.asitplus.jsonpath.core.JsonPathFilterExpressionType +import at.asitplus.jsonpath.core.JsonPathFunctionExtension +import at.asitplus.jsonpath.implementation.AntlrJsonPathCompilerErrorListener +import com.strumenta.antlrkotlin.runtime.BitSet +import io.github.aakira.napier.Napier +import org.antlr.v4.kotlinruntime.Parser +import org.antlr.v4.kotlinruntime.RecognitionException +import org.antlr.v4.kotlinruntime.Recognizer +import org.antlr.v4.kotlinruntime.atn.ATNConfigSet +import org.antlr.v4.kotlinruntime.dfa.DFA + +internal actual val defaultErrorListener = object : AntlrJsonPathCompilerErrorListener { + override fun unknownFunctionExtension(functionExtensionName: String) { + Napier.e { + "Unknown JSONPath function extension: \"$functionExtensionName\"" + } + } + + override fun invalidFunctionExtensionForTestExpression(functionExtensionName: String) { + Napier.e { + "Invalid JSONPath function extension return type for test expression: \"$functionExtensionName\"" + } + } + + override fun invalidFunctionExtensionForComparable(functionExtensionName: String) { + Napier.e { + "Invalid JSONPath function extension return type for comparable expression: \"$functionExtensionName\"" + } + } + + override fun invalidArglistForFunctionExtension( + functionExtensionName: String, + functionExtensionImplementation: JsonPathFunctionExtension, + coercedArgumentTypes: List> + ) { + Napier.e { + "Invalid arguments for function extension \"$functionExtensionName\": Expected: <${ + functionExtensionImplementation.argumentTypes.joinToString( + ", " + ) + }>, but received <${ + coercedArgumentTypes.map { it.first }.joinToString(", ") + }>: <${coercedArgumentTypes.map { it.second }.joinToString(", ")}>" + } + } + + override fun invalidTestExpression(testContextString: String) { + Napier.e { + "Invalid test expression: $testContextString" + } + } + + override fun reportAmbiguity( + recognizer: Parser, + dfa: DFA, + startIndex: Int, + stopIndex: Int, + exact: Boolean, + ambigAlts: BitSet, + configs: ATNConfigSet + ) { + // noop + } + + override fun reportAttemptingFullContext( + recognizer: Parser, + dfa: DFA, + startIndex: Int, + stopIndex: Int, + conflictingAlts: BitSet, + configs: ATNConfigSet + ) { + // noop + } + + override fun reportContextSensitivity( + recognizer: Parser, + dfa: DFA, + startIndex: Int, + stopIndex: Int, + prediction: Int, + configs: ATNConfigSet + ) { + // noop + } + + override fun syntaxError( + recognizer: Recognizer<*, *>, + offendingSymbol: Any?, + line: Int, + charPositionInLine: Int, + msg: String, + e: RecognitionException? + ) { + Napier.e { + "Syntax error $line:$charPositionInLine $msg" + } + } +} \ No newline at end of file diff --git a/jsonpath4k/src/linuxMain/kotlin/at/asitplus/jsonpath/JsonPath.linux.kt b/jsonpath4k/src/linuxMain/kotlin/at/asitplus/jsonpath/JsonPath.linux.kt new file mode 100644 index 0000000..aa8bf94 --- /dev/null +++ b/jsonpath4k/src/linuxMain/kotlin/at/asitplus/jsonpath/JsonPath.linux.kt @@ -0,0 +1,3 @@ +package at.asitplus.jsonpath + +internal actual val defaultErrorListener = noopErrorListener \ No newline at end of file diff --git a/jsonpath4k/src/macosMain/kotlin/at/asitplus/jsonpath/JsonPath.macos.kt b/jsonpath4k/src/macosMain/kotlin/at/asitplus/jsonpath/JsonPath.macos.kt new file mode 100644 index 0000000..7f6eb54 --- /dev/null +++ b/jsonpath4k/src/macosMain/kotlin/at/asitplus/jsonpath/JsonPath.macos.kt @@ -0,0 +1,101 @@ +package at.asitplus.jsonpath + +import at.asitplus.jsonpath.core.JsonPathFilterExpressionType +import at.asitplus.jsonpath.core.JsonPathFunctionExtension +import at.asitplus.jsonpath.implementation.AntlrJsonPathCompilerErrorListener +import com.strumenta.antlrkotlin.runtime.BitSet +import io.github.aakira.napier.Napier +import org.antlr.v4.kotlinruntime.Parser +import org.antlr.v4.kotlinruntime.RecognitionException +import org.antlr.v4.kotlinruntime.Recognizer +import org.antlr.v4.kotlinruntime.atn.ATNConfigSet +import org.antlr.v4.kotlinruntime.dfa.DFA + +internal actual val defaultErrorListener = object : AntlrJsonPathCompilerErrorListener { + override fun unknownFunctionExtension(functionExtensionName: String) { + Napier.e { + "Unknown JSONPath function extension: \"$functionExtensionName\"" + } + } + + override fun invalidFunctionExtensionForTestExpression(functionExtensionName: String) { + Napier.e { + "Invalid JSONPath function extension return type for test expression: \"$functionExtensionName\"" + } + } + + override fun invalidFunctionExtensionForComparable(functionExtensionName: String) { + Napier.e { + "Invalid JSONPath function extension return type for comparable expression: \"$functionExtensionName\"" + } + } + + override fun invalidArglistForFunctionExtension( + functionExtensionName: String, + functionExtensionImplementation: JsonPathFunctionExtension, + coercedArgumentTypes: List> + ) { + Napier.e { + "Invalid arguments for function extension \"$functionExtensionName\": Expected: <${ + functionExtensionImplementation.argumentTypes.joinToString( + ", " + ) + }>, but received <${ + coercedArgumentTypes.map { it.first }.joinToString(", ") + }>: <${coercedArgumentTypes.map { it.second }.joinToString(", ")}>" + } + } + + override fun invalidTestExpression(testContextString: String) { + Napier.e { + "Invalid test expression: $testContextString" + } + } + + override fun reportAmbiguity( + recognizer: Parser, + dfa: DFA, + startIndex: Int, + stopIndex: Int, + exact: Boolean, + ambigAlts: BitSet, + configs: ATNConfigSet + ) { + // noop + } + + override fun reportAttemptingFullContext( + recognizer: Parser, + dfa: DFA, + startIndex: Int, + stopIndex: Int, + conflictingAlts: BitSet, + configs: ATNConfigSet + ) { + // noop + } + + override fun reportContextSensitivity( + recognizer: Parser, + dfa: DFA, + startIndex: Int, + stopIndex: Int, + prediction: Int, + configs: ATNConfigSet + ) { + // noop + } + + override fun syntaxError( + recognizer: Recognizer<*, *>, + offendingSymbol: Any?, + line: Int, + charPositionInLine: Int, + msg: String, + e: RecognitionException? + ) { + Napier.e { + "Syntax error $line:$charPositionInLine $msg" + } + } +} \ No newline at end of file diff --git a/jsonpath4k/src/mingwMain/kotlin/at/asitplus/jsonpath/JsonPath.mingw.kt b/jsonpath4k/src/mingwMain/kotlin/at/asitplus/jsonpath/JsonPath.mingw.kt new file mode 100644 index 0000000..aa8bf94 --- /dev/null +++ b/jsonpath4k/src/mingwMain/kotlin/at/asitplus/jsonpath/JsonPath.mingw.kt @@ -0,0 +1,3 @@ +package at.asitplus.jsonpath + +internal actual val defaultErrorListener = noopErrorListener \ No newline at end of file diff --git a/jsonpath4k/src/nativeTest/kotlin/at/asitplus/jsonpath/AntlrConcurrencyNativeTest.kt b/jsonpath4k/src/nativeTest/kotlin/at/asitplus/jsonpath/AntlrConcurrencyNativeTest.kt index 725f35f..2e0f546 100644 --- a/jsonpath4k/src/nativeTest/kotlin/at/asitplus/jsonpath/AntlrConcurrencyNativeTest.kt +++ b/jsonpath4k/src/nativeTest/kotlin/at/asitplus/jsonpath/AntlrConcurrencyNativeTest.kt @@ -1,113 +1,56 @@ -@file:OptIn(DelicateCoroutinesApi::class) - package at.asitplus.jsonpath -import de.infix.testBalloon.framework.core.TestConfig -import de.infix.testBalloon.framework.core.testScope -import de.infix.testBalloon.framework.core.testSuite -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.newFixedThreadPoolContext -import kotlinx.coroutines.withTimeout -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonElement -import kotlin.coroutines.CoroutineContext -import kotlin.time.Duration.Companion.seconds +import at.asitplus.testballoon.matrix.matrixSuite +import kotlin.concurrent.AtomicInt +import kotlin.native.concurrent.TransferMode +import kotlin.native.concurrent.Worker /** * Highly-concurrent stress tests that hammer the ANTLR-generated lexer/parser (via [JsonPath]) from many - * threads at once. + * **real OS threads** at once, using Kotlin/Native [Worker]s (no coroutines — see + * [AntlrCoroutineConcurrencyNativeTest] for the coroutine-fan-out flavour). * * antlr-kotlin is **not** thread-safe: a generated `Lexer`/`Parser` shares process-global, mutable prediction * state through the companion object — the decoded `ATN`, the `decisionToDFA` array, and the * `PredictionContextCache`. `adaptivePredict` mutates those shared `DFA` structures (adding states/edges) with * no locking, so compiling JSONPath expressions concurrently races on that shared state. On Kotlin/Native - * (strict, no forgiving JIT) this surfaces as thrown exceptions (NPE, index-out-of-bounds, illegal state) or, + * (strict, no forgiving JIT) this surfaces as thrown exceptions (index-out-of-bounds, NPE, illegal state) or, * worse, silently wrong parses — and can even hard-crash the worker. * - * These tests are therefore **expected to FAIL** against an unpatched antlr-kotlin: they assert that no worker - * threw and that every concurrent compile produced the same result as a single-threaded baseline. They exist to - * (a) demonstrate the defect and (b) measure how far a thread-safety patch to antlr-kotlin gets us. - * - * `TestScope` (virtual time) is disabled per test: matrix/coroutine real concurrency cannot run under virtual - * time, and we deliberately want real threads. + * These tests are therefore **expected to FAIL** against an unpatched antlr-kotlin. They exist to demonstrate + * the defect and to measure how far a thread-safety patch gets us. */ private const val WORKERS = 64 private const val ITERATIONS = 250 private const val ROUNDS = 8 -private val realConcurrency = TestConfig.testScope(isEnabled = false) - -// Non-trivial, valid expressions. Filters, function extensions, logical operators, slices and descendant -// segments all force the parser through lots of ATN prediction / DFA construction — the racy hot path. -private val bookstoreExpressions = listOf( - "$.store.book[*].author", - "$..author", - "$.store.*", - "$.store..price", - "$..book[2]", - "$..book[-1]", - "$..book[0,1]", - "$..book[:2]", - "$..book[?@.isbn]", - "$..book[?@.price<10]", - "$..*", - "$.store.book[?@.price < 10 && @.category == 'fiction'].title", - "$.store.book[?match(@.category, 'fic.*')].author", - "$.store.book[?search(@.title, 'the')].title", -) - -// Expressions that only need to *compile* (type-checkable standalone) — used for the pure-compile race. -private val compileOnlyExpressions = bookstoreExpressions + listOf( - "$[?length(@) < 3]", - "$[?count(@.*) == 1]", - "$[?value(@..color) == 'red']", - "$[?match(@.timezone, 'Europe/.*')]", - "$[?@.a || @.b && @.c]", - "$[1:5:2]", - "$.o[?@<3, ?@<3]", +/** Everything a worker thread needs, handed over via the [Worker.execute] producer (no captured state). */ +private class WorkerInput( + val worker: Int, + val gate: AtomicInt, + val ready: AtomicInt, + val block: (worker: Int, iteration: Int) -> Unit, ) -private val bookStore: JsonElement = Json.decodeFromString( - """ - { "store": { - "book": [ - { "category": "reference", "author": "Nigel Rees", "title": "Sayings of the Century", "price": 8.95 }, - { "category": "fiction", "author": "Evelyn Waugh", "title": "Sword of Honour", "price": 12.99 }, - { "category": "fiction", "author": "Herman Melville", "title": "Moby Dick", "isbn": "0-553-21311-3", "price": 8.99 }, - { "category": "fiction", "author": "J. R. R. Tolkien", "title": "The Lord of the Rings", "isbn": "0-395-19395-8", "price": 22.99 } - ], - "bicycle": { "color": "red", "price": 399 } - } - } - """.trimIndent() -) - -private class WrongResult(message: String) : Exception(message) - /** - * Runs [block] on [WORKERS] coroutines dispatched to a real, multi-threaded pool. All workers wait on a start - * gate so they hit the hot path simultaneously (maximizing the race window), then each runs [ITERATIONS] times. - * Each worker collects its own failures locally (no shared mutable state), aggregated after `awaitAll`. + * Spawns [WORKERS] real threads, each parked on a spin-gate, then releases them all at once so they collide on + * the *cold* shared ANTLR state simultaneously (the tightest race window). Each thread runs [block] + * [ITERATIONS] times and returns the throwables it caught; those are aggregated after every thread joins. */ -@OptIn(DelicateCoroutinesApi::class) -private suspend fun stress( - label: String, - dispatcher: CoroutineContext, - block: (worker: Int, iteration: Int) -> Unit, -): List = coroutineScope { - val gate = CompletableDeferred() - val deferred = (0 until WORKERS).map { worker -> - async(dispatcher) { - gate.await() +private fun stress(label: String, block: (worker: Int, iteration: Int) -> Unit) { + val gate = AtomicInt(0) // 0 = closed, 1 = released + val ready = AtomicInt(0) // number of threads parked at the gate + val workers = List(WORKERS) { Worker.start(name = "antlr-$label-$it") } + + val futures = workers.mapIndexed { index, worker -> + worker.execute(TransferMode.SAFE, { WorkerInput(index, gate, ready, block) }) { input -> + input.ready.incrementAndGet() + while (input.gate.value == 0) { /* spin until every thread is parked and the gate opens */ } val local = ArrayList() repeat(ITERATIONS) { iteration -> try { - block(worker, iteration) + input.block(input.worker, iteration) } catch (t: Throwable) { local += t } @@ -115,84 +58,56 @@ private suspend fun stress( local } } - gate.complete(Unit) // release all workers at once - deferred.awaitAll().flatten().also { failures -> - if (failures.isNotEmpty()) { - val summary = failures - .groupingBy { "${it::class.simpleName}: ${it.message}" } - .eachCount() - .entries - .sortedByDescending { it.value } - .joinToString("\n") { (kind, count) -> " x$count $kind" } - throw AssertionError( - "[$label] ${failures.size} of ${WORKERS * ITERATIONS} concurrent ANTLR operations failed " + - "(antlr-kotlin is not thread-safe). Distinct failures:\n$summary" - ) - } - } + + while (ready.value < WORKERS) { /* wait until all threads are parked */ } + gate.value = 1 // release all WORKERS at the same instant + + val failures = futures.flatMap { it.result } // joins each worker + workers.forEach { it.requestTermination().result } + + if (failures.isNotEmpty()) throw concurrencyFailure(label, WORKERS * ITERATIONS, failures) } -val AntlrConcurrencyNativeTest by testSuite { +val AntlrConcurrencyNativeTest by matrixSuite { - // 1) COLD DFA, SAME EXPRESSION: every worker compiles the identical complex expression with no warmup, so + // 1) COLD DFA, SAME EXPRESSION: every thread compiles the identical complex expression with no warmup, so // they all race to build the very same shared DFA decision states at once — the tightest contention. test("concurrent compilation of the same expression on a cold parser", testConfig = realConcurrency) { - val pool = newFixedThreadPoolContext(WORKERS, "antlr-same") - try { - withTimeout(120.seconds) { - repeat(ROUNDS) { - val expr = "$.store.book[?@.price < 10 && @.category == 'fiction'].title" - stress("same-expr", pool) { _, _ -> - JsonPath(expr) - } - } + repeat(ROUNDS) { + val expr = "$.store.book[?@.price < 10 && @.category == 'fiction'].title" + stress("same-expr") { _, _ -> + JsonPath(expr) } - } finally { - pool.close() } } - // 2) MANY DISTINCT EXPRESSIONS: workers pick different expressions, maximizing churn across many decisions + // 2) MANY DISTINCT EXPRESSIONS: threads pick different expressions, maximizing churn across many decisions // and the shared PredictionContextCache simultaneously. test("concurrent compilation of many distinct expressions", testConfig = realConcurrency) { - val pool = newFixedThreadPoolContext(WORKERS, "antlr-distinct") - try { - withTimeout(120.seconds) { - repeat(ROUNDS) { - stress("distinct-expr", pool) { worker, iteration -> - val expr = compileOnlyExpressions[(worker + iteration) % compileOnlyExpressions.size] - JsonPath(expr) - } - } + repeat(ROUNDS) { + stress("distinct-expr") { worker, iteration -> + val expr = compileOnlyExpressions[(worker + iteration) % compileOnlyExpressions.size] + JsonPath(expr) } - } finally { - pool.close() } } // 3) CORRECTNESS UNDER CONCURRENCY: compile + query in parallel and compare against a single-threaded // baseline. Catches silent corruption (no exception, but a wrong/incomplete parse -> wrong node list). test("concurrent compilation yields results consistent with a single-threaded baseline", testConfig = realConcurrency) { - // Baseline computed sequentially, before any concurrency touches the shared ANTLR state. + // Baseline computed before any thread touches the shared ANTLR state. val baseline: Map = bookstoreExpressions.associateWith { expr -> JsonPath(expr).query(bookStore).map { it.value }.toString() } - val pool = newFixedThreadPoolContext(WORKERS, "antlr-correct") - try { - withTimeout(120.seconds) { - repeat(ROUNDS) { - stress("correctness", pool) { worker, iteration -> - val expr = bookstoreExpressions[(worker + iteration) % bookstoreExpressions.size] - val actual = JsonPath(expr).query(bookStore).map { it.value }.toString() - val expected = baseline.getValue(expr) - if (actual != expected) { - throw WrongResult("expr <$expr>: expected $expected but got $actual") - } - } + repeat(ROUNDS) { + stress("correctness") { worker, iteration -> + val expr = bookstoreExpressions[(worker + iteration) % bookstoreExpressions.size] + val actual = JsonPath(expr).query(bookStore).map { it.value }.toString() + val expected = baseline.getValue(expr) + if (actual != expected) { + throw WrongResult("expr <$expr>: expected $expected but got $actual") } } - } finally { - pool.close() } } } diff --git a/jsonpath4k/src/nativeTest/kotlin/at/asitplus/jsonpath/AntlrCoroutineConcurrencyNativeTest.kt b/jsonpath4k/src/nativeTest/kotlin/at/asitplus/jsonpath/AntlrCoroutineConcurrencyNativeTest.kt new file mode 100644 index 0000000..adc4fb8 --- /dev/null +++ b/jsonpath4k/src/nativeTest/kotlin/at/asitplus/jsonpath/AntlrCoroutineConcurrencyNativeTest.kt @@ -0,0 +1,115 @@ +package at.asitplus.jsonpath + +import at.asitplus.testballoon.matrix.matrixSuite +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout +import kotlin.time.Duration.Companion.seconds + +/** + * Coroutine-fan-out sibling of [AntlrConcurrencyNativeTest]. + * + * Instead of a bounded, dedicated thread pool, this fires **500 coroutines at once** with `launch` onto + * [Dispatchers.Default] (which is multi-threaded on Kotlin/Native). The enclosing `coroutineScope` suspends + * until all of them finish (structured concurrency joins the children for us — no manual gate/`joinAll`), so + * they slam the ANTLR-generated parser's process-global, mutable prediction state (decoded `ATN`, + * `decisionToDFA`, `PredictionContextCache`) concurrently. + * + * antlr-kotlin does no locking around that state, so concurrent `JsonPath(...)` compilation races on it. Like + * the thread-pool suite, these tests are **expected to FAIL** against an unpatched antlr-kotlin (thrown + * exceptions and/or a wrong parse vs. a single-threaded baseline) and serve to measure a thread-safety patch. + * + * `TestScope` (virtual time) is disabled per test: real concurrency cannot run under virtual time. + */ + +private const val COROUTINES = 500 +private const val ITERATIONS = 40 +private const val ROUNDS = 8 + +/** + * Launches [COROUTINES] coroutines on [Dispatchers.Default], each running [block] [ITERATIONS] times, and lets + * the surrounding `coroutineScope` join them all. Failures are funneled through an unlimited [Channel] + * (thread-safe `trySend`), then drained after the scope completes — no shared mutable collection. + */ +private suspend fun coroutineStress( + label: String, + block: (worker: Int, iteration: Int) -> Unit, +) { + val failures = Channel(Channel.UNLIMITED) + coroutineScope { + // Start barrier: every coroutine parks here (cheap, no parser work) until all are launched, then all + // are released at once so they collide on the *cold* shared ANTLR state simultaneously. Without it the + // first coroutines fully build the DFA before the rest start, closing the race window (test goes green). + val gate = CompletableDeferred() + repeat(COROUTINES) { worker -> + launch(Dispatchers.Default) { + gate.await() + repeat(ITERATIONS) { iteration -> + try { + block(worker, iteration) + } catch (t: Throwable) { + failures.trySend(t) + } + } + } + } + gate.complete(Unit) // release all COROUTINES at once + } // coroutineScope suspends here until all coroutines complete + failures.close() + + val collected = ArrayList() + while (true) { + collected += failures.tryReceive().getOrNull() ?: break + } + if (collected.isNotEmpty()) throw concurrencyFailure(label, COROUTINES * ITERATIONS, collected) +} + +val AntlrCoroutineConcurrencyNativeTest by matrixSuite { + + // 1) COLD DFA, SAME EXPRESSION: 500 coroutines compile the identical complex expression with no warmup. + test("500 coroutines compiling the same expression on a cold parser", testConfig = realConcurrency) { + withTimeout(120.seconds) { + repeat(ROUNDS) { + val expr = "$.store.book[?@.price < 10 && @.category == 'fiction'].title" + coroutineStress("same-expr") { _, _ -> + JsonPath(expr) + } + } + } + } + + // 2) MANY DISTINCT EXPRESSIONS: 500 coroutines spread across many expressions -> churn across many decisions. + test("500 coroutines compiling many distinct expressions", testConfig = realConcurrency) { + withTimeout(120.seconds) { + repeat(ROUNDS) { + coroutineStress("distinct-expr") { worker, iteration -> + val expr = compileOnlyExpressions[(worker + iteration) % compileOnlyExpressions.size] + JsonPath(expr) + } + } + } + } + + // 3) CORRECTNESS UNDER CONCURRENCY: compile + query in parallel, compare against a single-threaded baseline + // to catch silent corruption (no exception, but a wrong/incomplete parse -> wrong node list). + test("500 coroutines produce results consistent with a single-threaded baseline", testConfig = realConcurrency) { + val baseline: Map = bookstoreExpressions.associateWith { expr -> + JsonPath(expr).query(bookStore).map { it.value }.toString() + } + withTimeout(120.seconds) { + repeat(ROUNDS) { + coroutineStress("correctness") { worker, iteration -> + val expr = bookstoreExpressions[(worker + iteration) % bookstoreExpressions.size] + val actual = JsonPath(expr).query(bookStore).map { it.value }.toString() + val expected = baseline.getValue(expr) + if (actual != expected) { + throw WrongResult("expr <$expr>: expected $expected but got $actual") + } + } + } + } + } +} diff --git a/jsonpath4k/src/nativeTest/kotlin/at/asitplus/jsonpath/ConcurrencyStressFixtures.kt b/jsonpath4k/src/nativeTest/kotlin/at/asitplus/jsonpath/ConcurrencyStressFixtures.kt new file mode 100644 index 0000000..4df51bd --- /dev/null +++ b/jsonpath4k/src/nativeTest/kotlin/at/asitplus/jsonpath/ConcurrencyStressFixtures.kt @@ -0,0 +1,78 @@ +package at.asitplus.jsonpath + +import de.infix.testBalloon.framework.core.TestConfig +import de.infix.testBalloon.framework.core.testScope +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement + +/** + * Shared fixtures for the ANTLR concurrency stress suites ([AntlrConcurrencyNativeTest] uses a dedicated thread + * pool; [AntlrCoroutineConcurrencyNativeTest] launches many coroutines). Extracted so the two suites don't + * duplicate the data — and because private top-level *classes* (unlike vals/funs) share the package namespace, + * so declaring `WrongResult` in both files would collide. + */ + +/** Real (non-virtual-time) execution: real concurrency cannot run under TestBalloon's virtual-time `TestScope`. */ +internal val realConcurrency: TestConfig = TestConfig.testScope(isEnabled = false) + +// Non-trivial, valid expressions. Filters, function extensions, logical operators, slices and descendant +// segments all force the parser through lots of ATN prediction / DFA construction — the racy hot path. +internal val bookstoreExpressions = listOf( + "$.store.book[*].author", + "$..author", + "$.store.*", + "$.store..price", + "$..book[2]", + "$..book[-1]", + "$..book[0,1]", + "$..book[:2]", + "$..book[?@.isbn]", + "$..book[?@.price<10]", + "$..*", + "$.store.book[?@.price < 10 && @.category == 'fiction'].title", + "$.store.book[?match(@.category, 'fic.*')].author", + "$.store.book[?search(@.title, 'the')].title", +) + +// Expressions that only need to *compile* (type-checkable standalone) — used for the pure-compile race. +internal val compileOnlyExpressions = bookstoreExpressions + listOf( + "$[?length(@) < 3]", + "$[?count(@.*) == 1]", + "$[?value(@..color) == 'red']", + "$[?match(@.timezone, 'Europe/.*')]", + "$[?@.a || @.b && @.c]", + "$[1:5:2]", + "$.o[?@<3, ?@<3]", +) + +internal val bookStore: JsonElement = Json.decodeFromString( + """ + { "store": { + "book": [ + { "category": "reference", "author": "Nigel Rees", "title": "Sayings of the Century", "price": 8.95 }, + { "category": "fiction", "author": "Evelyn Waugh", "title": "Sword of Honour", "price": 12.99 }, + { "category": "fiction", "author": "Herman Melville", "title": "Moby Dick", "isbn": "0-553-21311-3", "price": 8.99 }, + { "category": "fiction", "author": "J. R. R. Tolkien", "title": "The Lord of the Rings", "isbn": "0-395-19395-8", "price": 22.99 } + ], + "bicycle": { "color": "red", "price": 399 } + } + } + """.trimIndent() +) + +/** Thrown when a concurrent compile+query produced a different result than the single-threaded baseline. */ +internal class WrongResult(message: String) : Exception(message) + +/** Formats collected concurrent failures into an [AssertionError] message, grouped by kind. */ +internal fun concurrencyFailure(label: String, total: Int, failures: List): AssertionError { + val summary = failures + .groupingBy { "${it::class.simpleName}: ${it.message}" } + .eachCount() + .entries + .sortedByDescending { it.value } + .joinToString("\n") { (kind, count) -> " x$count $kind" } + return AssertionError( + "[$label] ${failures.size} of $total concurrent ANTLR operations failed " + + "(antlr-kotlin is not thread-safe). Distinct failures:\n$summary" + ) +} diff --git a/jsonpath4k/src/tvosMain/kotlin/at/asitplus/jsonpath/JsonPath.tvos.kt b/jsonpath4k/src/tvosMain/kotlin/at/asitplus/jsonpath/JsonPath.tvos.kt new file mode 100644 index 0000000..7f6eb54 --- /dev/null +++ b/jsonpath4k/src/tvosMain/kotlin/at/asitplus/jsonpath/JsonPath.tvos.kt @@ -0,0 +1,101 @@ +package at.asitplus.jsonpath + +import at.asitplus.jsonpath.core.JsonPathFilterExpressionType +import at.asitplus.jsonpath.core.JsonPathFunctionExtension +import at.asitplus.jsonpath.implementation.AntlrJsonPathCompilerErrorListener +import com.strumenta.antlrkotlin.runtime.BitSet +import io.github.aakira.napier.Napier +import org.antlr.v4.kotlinruntime.Parser +import org.antlr.v4.kotlinruntime.RecognitionException +import org.antlr.v4.kotlinruntime.Recognizer +import org.antlr.v4.kotlinruntime.atn.ATNConfigSet +import org.antlr.v4.kotlinruntime.dfa.DFA + +internal actual val defaultErrorListener = object : AntlrJsonPathCompilerErrorListener { + override fun unknownFunctionExtension(functionExtensionName: String) { + Napier.e { + "Unknown JSONPath function extension: \"$functionExtensionName\"" + } + } + + override fun invalidFunctionExtensionForTestExpression(functionExtensionName: String) { + Napier.e { + "Invalid JSONPath function extension return type for test expression: \"$functionExtensionName\"" + } + } + + override fun invalidFunctionExtensionForComparable(functionExtensionName: String) { + Napier.e { + "Invalid JSONPath function extension return type for comparable expression: \"$functionExtensionName\"" + } + } + + override fun invalidArglistForFunctionExtension( + functionExtensionName: String, + functionExtensionImplementation: JsonPathFunctionExtension, + coercedArgumentTypes: List> + ) { + Napier.e { + "Invalid arguments for function extension \"$functionExtensionName\": Expected: <${ + functionExtensionImplementation.argumentTypes.joinToString( + ", " + ) + }>, but received <${ + coercedArgumentTypes.map { it.first }.joinToString(", ") + }>: <${coercedArgumentTypes.map { it.second }.joinToString(", ")}>" + } + } + + override fun invalidTestExpression(testContextString: String) { + Napier.e { + "Invalid test expression: $testContextString" + } + } + + override fun reportAmbiguity( + recognizer: Parser, + dfa: DFA, + startIndex: Int, + stopIndex: Int, + exact: Boolean, + ambigAlts: BitSet, + configs: ATNConfigSet + ) { + // noop + } + + override fun reportAttemptingFullContext( + recognizer: Parser, + dfa: DFA, + startIndex: Int, + stopIndex: Int, + conflictingAlts: BitSet, + configs: ATNConfigSet + ) { + // noop + } + + override fun reportContextSensitivity( + recognizer: Parser, + dfa: DFA, + startIndex: Int, + stopIndex: Int, + prediction: Int, + configs: ATNConfigSet + ) { + // noop + } + + override fun syntaxError( + recognizer: Recognizer<*, *>, + offendingSymbol: Any?, + line: Int, + charPositionInLine: Int, + msg: String, + e: RecognitionException? + ) { + Napier.e { + "Syntax error $line:$charPositionInLine $msg" + } + } +} \ No newline at end of file diff --git a/jsonpath4k/src/watchosMain/kotlin/at/asitplus/jsonpath/JsonPath.watchos.kt b/jsonpath4k/src/watchosMain/kotlin/at/asitplus/jsonpath/JsonPath.watchos.kt new file mode 100644 index 0000000..aa8bf94 --- /dev/null +++ b/jsonpath4k/src/watchosMain/kotlin/at/asitplus/jsonpath/JsonPath.watchos.kt @@ -0,0 +1,3 @@ +package at.asitplus.jsonpath + +internal actual val defaultErrorListener = noopErrorListener \ No newline at end of file diff --git a/jsonpath4k/src/webMain/kotlin/at/asitplus/jsonpath/JsonPath.web.kt b/jsonpath4k/src/webMain/kotlin/at/asitplus/jsonpath/JsonPath.web.kt new file mode 100644 index 0000000..bd52b3e --- /dev/null +++ b/jsonpath4k/src/webMain/kotlin/at/asitplus/jsonpath/JsonPath.web.kt @@ -0,0 +1,103 @@ +package at.asitplus.jsonpath + +import at.asitplus.jsonpath.core.JsonPathCompiler +import at.asitplus.jsonpath.core.JsonPathFilterExpressionType +import at.asitplus.jsonpath.core.JsonPathFunctionExtension +import at.asitplus.jsonpath.implementation.AntlrJsonPathCompiler +import at.asitplus.jsonpath.implementation.AntlrJsonPathCompilerErrorListener +import com.strumenta.antlrkotlin.runtime.BitSet +import io.github.aakira.napier.Napier +import org.antlr.v4.kotlinruntime.Parser +import org.antlr.v4.kotlinruntime.RecognitionException +import org.antlr.v4.kotlinruntime.Recognizer +import org.antlr.v4.kotlinruntime.atn.ATNConfigSet +import org.antlr.v4.kotlinruntime.dfa.DFA + +internal actual val defaultErrorListener= object : AntlrJsonPathCompilerErrorListener { + override fun unknownFunctionExtension(functionExtensionName: String) { + Napier.e { + "Unknown JSONPath function extension: \"$functionExtensionName\"" + } + } + + override fun invalidFunctionExtensionForTestExpression(functionExtensionName: String) { + Napier.e { + "Invalid JSONPath function extension return type for test expression: \"$functionExtensionName\"" + } + } + + override fun invalidFunctionExtensionForComparable(functionExtensionName: String) { + Napier.e { + "Invalid JSONPath function extension return type for comparable expression: \"$functionExtensionName\"" + } + } + + override fun invalidArglistForFunctionExtension( + functionExtensionName: String, + functionExtensionImplementation: JsonPathFunctionExtension, + coercedArgumentTypes: List> + ) { + Napier.e { + "Invalid arguments for function extension \"$functionExtensionName\": Expected: <${ + functionExtensionImplementation.argumentTypes.joinToString( + ", " + ) + }>, but received <${ + coercedArgumentTypes.map { it.first }.joinToString(", ") + }>: <${coercedArgumentTypes.map { it.second }.joinToString(", ")}>" + } + } + + override fun invalidTestExpression(testContextString: String) { + Napier.e { + "Invalid test expression: $testContextString" + } + } + + override fun reportAmbiguity( + recognizer: Parser, + dfa: DFA, + startIndex: Int, + stopIndex: Int, + exact: Boolean, + ambigAlts: BitSet, + configs: ATNConfigSet + ) { + // noop + } + + override fun reportAttemptingFullContext( + recognizer: Parser, + dfa: DFA, + startIndex: Int, + stopIndex: Int, + conflictingAlts: BitSet, + configs: ATNConfigSet + ) { + // noop + } + + override fun reportContextSensitivity( + recognizer: Parser, + dfa: DFA, + startIndex: Int, + stopIndex: Int, + prediction: Int, + configs: ATNConfigSet + ) { + // noop + } + + override fun syntaxError( + recognizer: Recognizer<*, *>, + offendingSymbol: Any?, + line: Int, + charPositionInLine: Int, + msg: String, + e: RecognitionException? + ) { + Napier.e { + "Syntax error $line:$charPositionInLine $msg" + } + } + } \ No newline at end of file From 4e5c1315258a6abf9663f2b678d374a882611b61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Wed, 22 Jul 2026 19:45:59 +0200 Subject: [PATCH 04/10] better testing --- .github/workflows/test-jvm.yml | 4 +- .../jsonpath/AntlrConcurrencyNativeTest.kt | 9 ++-- .../AntlrCoroutineConcurrencyNativeTest.kt | 19 ++++---- .../jsonpath/ConcurrencyStressFixtures.kt | 46 +++++++++++++++++-- 4 files changed, 61 insertions(+), 17 deletions(-) diff --git a/.github/workflows/test-jvm.yml b/.github/workflows/test-jvm.yml index bae0124..ce61e32 100644 --- a/.github/workflows/test-jvm.yml +++ b/.github/workflows/test-jvm.yml @@ -1,4 +1,4 @@ -name: Test JVM implementation +name: Test all non-Apple implementations on: [push] jobs: build: @@ -13,7 +13,7 @@ jobs: distribution: 'temurin' java-version: '17' - name: Run tests - run: ./gradlew jvmTest + run: ./gradlew allTests - name: Test Report uses: dorny/test-reporter@v1 if: success() || failure() diff --git a/jsonpath4k/src/nativeTest/kotlin/at/asitplus/jsonpath/AntlrConcurrencyNativeTest.kt b/jsonpath4k/src/nativeTest/kotlin/at/asitplus/jsonpath/AntlrConcurrencyNativeTest.kt index 2e0f546..d711b0b 100644 --- a/jsonpath4k/src/nativeTest/kotlin/at/asitplus/jsonpath/AntlrConcurrencyNativeTest.kt +++ b/jsonpath4k/src/nativeTest/kotlin/at/asitplus/jsonpath/AntlrConcurrencyNativeTest.kt @@ -21,9 +21,12 @@ import kotlin.native.concurrent.Worker * the defect and to measure how far a thread-safety patch gets us. */ -private const val WORKERS = 64 -private const val ITERATIONS = 250 -private const val ROUNDS = 8 +// Sized per target (see threadStress). The race triggers by *oversubscription* — many more threads than cores +// forces heavy interleaving — so it reproduces regardless of a runner's core count; the ~8 MB stack per Worker +// is the memory ceiling that caps the fan-out on constrained runners. +private val WORKERS = threadStress.fanOut +private val ITERATIONS = threadStress.iterations +private val ROUNDS = threadStress.rounds /** Everything a worker thread needs, handed over via the [Worker.execute] producer (no captured state). */ private class WorkerInput( diff --git a/jsonpath4k/src/nativeTest/kotlin/at/asitplus/jsonpath/AntlrCoroutineConcurrencyNativeTest.kt b/jsonpath4k/src/nativeTest/kotlin/at/asitplus/jsonpath/AntlrCoroutineConcurrencyNativeTest.kt index adc4fb8..b593d13 100644 --- a/jsonpath4k/src/nativeTest/kotlin/at/asitplus/jsonpath/AntlrCoroutineConcurrencyNativeTest.kt +++ b/jsonpath4k/src/nativeTest/kotlin/at/asitplus/jsonpath/AntlrCoroutineConcurrencyNativeTest.kt @@ -25,9 +25,12 @@ import kotlin.time.Duration.Companion.seconds * `TestScope` (virtual time) is disabled per test: real concurrency cannot run under virtual time. */ -private const val COROUTINES = 500 -private const val ITERATIONS = 40 -private const val ROUNDS = 8 +// Sized per target (see coroutineStressProfile). Coroutines are cheap, but true parallelism is capped at +// Dispatchers.Default's size (~CPU cores), so this is a strong reproducer only on many-core hosts and a light +// smoke test on constrained simulators — the thread-based suite is the reliable gate. +private val COROUTINES = coroutineStressProfile.fanOut +private val ITERATIONS = coroutineStressProfile.iterations +private val ROUNDS = coroutineStressProfile.rounds /** * Launches [COROUTINES] coroutines on [Dispatchers.Default], each running [block] [ITERATIONS] times, and lets @@ -69,8 +72,8 @@ private suspend fun coroutineStress( val AntlrCoroutineConcurrencyNativeTest by matrixSuite { - // 1) COLD DFA, SAME EXPRESSION: 500 coroutines compile the identical complex expression with no warmup. - test("500 coroutines compiling the same expression on a cold parser", testConfig = realConcurrency) { + // 1) COLD DFA, SAME EXPRESSION: many coroutines compile the identical complex expression with no warmup. + test("$COROUTINES coroutines compiling the same expression on a cold parser", testConfig = realConcurrency) { withTimeout(120.seconds) { repeat(ROUNDS) { val expr = "$.store.book[?@.price < 10 && @.category == 'fiction'].title" @@ -81,8 +84,8 @@ val AntlrCoroutineConcurrencyNativeTest by matrixSuite { } } - // 2) MANY DISTINCT EXPRESSIONS: 500 coroutines spread across many expressions -> churn across many decisions. - test("500 coroutines compiling many distinct expressions", testConfig = realConcurrency) { + // 2) MANY DISTINCT EXPRESSIONS: coroutines spread across many expressions -> churn across many decisions. + test("$COROUTINES coroutines compiling many distinct expressions", testConfig = realConcurrency) { withTimeout(120.seconds) { repeat(ROUNDS) { coroutineStress("distinct-expr") { worker, iteration -> @@ -95,7 +98,7 @@ val AntlrCoroutineConcurrencyNativeTest by matrixSuite { // 3) CORRECTNESS UNDER CONCURRENCY: compile + query in parallel, compare against a single-threaded baseline // to catch silent corruption (no exception, but a wrong/incomplete parse -> wrong node list). - test("500 coroutines produce results consistent with a single-threaded baseline", testConfig = realConcurrency) { + test("$COROUTINES coroutines produce results consistent with a single-threaded baseline", testConfig = realConcurrency) { val baseline: Map = bookstoreExpressions.associateWith { expr -> JsonPath(expr).query(bookStore).map { it.value }.toString() } diff --git a/jsonpath4k/src/nativeTest/kotlin/at/asitplus/jsonpath/ConcurrencyStressFixtures.kt b/jsonpath4k/src/nativeTest/kotlin/at/asitplus/jsonpath/ConcurrencyStressFixtures.kt index 4df51bd..84c0372 100644 --- a/jsonpath4k/src/nativeTest/kotlin/at/asitplus/jsonpath/ConcurrencyStressFixtures.kt +++ b/jsonpath4k/src/nativeTest/kotlin/at/asitplus/jsonpath/ConcurrencyStressFixtures.kt @@ -1,20 +1,58 @@ +@file:OptIn(ExperimentalNativeApi::class) + package at.asitplus.jsonpath import de.infix.testBalloon.framework.core.TestConfig import de.infix.testBalloon.framework.core.testScope import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement +import kotlin.experimental.ExperimentalNativeApi +import kotlin.native.OsFamily +import kotlin.native.Platform /** - * Shared fixtures for the ANTLR concurrency stress suites ([AntlrConcurrencyNativeTest] uses a dedicated thread - * pool; [AntlrCoroutineConcurrencyNativeTest] launches many coroutines). Extracted so the two suites don't - * duplicate the data — and because private top-level *classes* (unlike vals/funs) share the package namespace, - * so declaring `WrongResult` in both files would collide. + * Shared fixtures for the ANTLR concurrency stress suites ([AntlrConcurrencyNativeTest] uses real OS threads; + * [AntlrCoroutineConcurrencyNativeTest] launches many coroutines). Extracted so the two suites don't duplicate + * the data — and because private top-level *classes* (unlike vals/funs) share the package namespace, so + * declaring `WrongResult` in both files would collide. */ /** Real (non-virtual-time) execution: real concurrency cannot run under TestBalloon's virtual-time `TestScope`. */ internal val realConcurrency: TestConfig = TestConfig.testScope(isEnabled = false) +/** How hard to hammer, per fan-out unit (OS thread or coroutine). */ +internal data class StressProfile(val fanOut: Int, val iterations: Int, val rounds: Int) + +/** + * Sizes the fan-out to the resources of a typical runner for the **current target** (via [Platform.osFamily]) — + * one shared `when` instead of an expect/actual per source set (this module has ~15 native targets, and the + * androidNative ones are conditional on the NDK). The whole suite still runs on every native target; it just + * scales down where resources are tight. + * + * Rationale: + * - LINUX: the dedicated full-throttle stress pipeline (`ubuntu-latest`: more cores + RAM) → go big. + * - MACOSX: run natively on dev Macs / the macOS CI host → heavy (this is what to run locally for a hard soak). + * - IOS/TVOS/WATCHOS/ANDROID: simulator/emulator on a constrained host (the macOS CI runner is ~3 cores / 7 GB, + * and every Kotlin/Native `Worker` costs an ~8 MB stack) → scale right down so the sim job stays fast/safe. + */ +internal val threadStress: StressProfile = when (Platform.osFamily) { + OsFamily.LINUX -> StressProfile(fanOut = 256, iterations = 200, rounds = 4) + OsFamily.MACOSX -> StressProfile(fanOut = 128, iterations = 200, rounds = 4) + OsFamily.WINDOWS -> StressProfile(fanOut = 64, iterations = 150, rounds = 3) + else -> StressProfile(fanOut = 12, iterations = 60, rounds = 2) +} + +/** + * Coroutines are cheap, but true parallelism is capped at `Dispatchers.Default` (~CPU cores), so this suite is a + * strong reproducer only on many-core hosts (LINUX/MACOSX) and a light smoke test on constrained simulators. + */ +internal val coroutineStressProfile: StressProfile = when (Platform.osFamily) { + OsFamily.LINUX -> StressProfile(fanOut = 20_000, iterations = 10, rounds = 4) + OsFamily.MACOSX -> StressProfile(fanOut = 10_000, iterations = 10, rounds = 4) + OsFamily.WINDOWS -> StressProfile(fanOut = 5_000, iterations = 10, rounds = 3) + else -> StressProfile(fanOut = 1_000, iterations = 8, rounds = 2) +} + // Non-trivial, valid expressions. Filters, function extensions, logical operators, slices and descendant // segments all force the parser through lots of ATN prediction / DFA construction — the racy hot path. internal val bookstoreExpressions = listOf( From 1bfbe230bcb471e05331a291983490bbc382d3b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Wed, 22 Jul 2026 19:50:14 +0200 Subject: [PATCH 05/10] remove deprecation --- CHANGELOG.md | 3 ++- gradle.properties | 2 +- .../at/asitplus/jsonpath/core/JsonPathFunctionExtension.kt | 3 --- .../kotlin/at/asitplus/jsonpath/core/NormalizedJsonPath.kt | 3 +-- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24fe3a1..8ac34a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # CHANGELOG -# Release 3.2.0-SNAPSHOT +# Release 4.0.0-SNAPSHOT * Support all KMP targets * Logging through Napier is only supported on * Android (ART) @@ -12,6 +12,7 @@ * Logging on other targets defaults to NOOP * Kotlin 2.4.0 * Fix race condition thanks to latest ANTLR 1.0.13 +* Remove deprecations # Release 3.1.0 * Add: structural equality check for `NormalizedJsonPath` and `NormalizedJsonPathSegment` diff --git a/gradle.properties b/gradle.properties index ee30cc0..a7a8439 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,7 +16,7 @@ kotlin.mpp.stability.nowarn=true kotlin.native.ignoreDisabledTargets=true -artifactVersion = 3.2.0-SNAPSHOT +artifactVersion = 4.0.0-SNAPSHOT jdk.version=17 android.minSdk=30 android.compileSdk=34 diff --git a/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/core/JsonPathFunctionExtension.kt b/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/core/JsonPathFunctionExtension.kt index b7ba251..bb4b04f 100644 --- a/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/core/JsonPathFunctionExtension.kt +++ b/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/core/JsonPathFunctionExtension.kt @@ -12,9 +12,6 @@ sealed class JsonPathFunctionExtension( ) { abstract operator fun invoke(arguments: List): JsonPathFilterExpressionValue - @Deprecated("To be removed in version 4. Replaced with `invoke`.", ReplaceWith("invoke")) - fun evaluate(arguments: List): JsonPathFilterExpressionValue = invoke(arguments) - class ValueTypeFunctionExtension( vararg argumentTypes: JsonPathFilterExpressionType, private val evaluator: (arguments: List) -> JsonElement?, diff --git a/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/core/NormalizedJsonPath.kt b/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/core/NormalizedJsonPath.kt index 56594fb..81e38ea 100644 --- a/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/core/NormalizedJsonPath.kt +++ b/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/core/NormalizedJsonPath.kt @@ -11,8 +11,7 @@ import kotlin.jvm.JvmInline @Serializable @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 = listOf(), + private val segments: List = listOf(), ) : List by segments { constructor(vararg segments: NormalizedJsonPathSegment) : this(segments = segments.asList()) From 95fb70ab2f1c3125bbbdd83a1677035280220069 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Wed, 22 Jul 2026 19:51:47 +0200 Subject: [PATCH 06/10] fix serializer names --- CHANGELOG.md | 1 + .../at/asitplus/jsonpath/core/NormalizedJsonPathSegment.kt | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ac34a8..66722a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ * Kotlin 2.4.0 * Fix race condition thanks to latest ANTLR 1.0.13 * Remove deprecations +* Change Serializer names to work on web targets too # Release 3.1.0 * Add: structural equality check for `NormalizedJsonPath` and `NormalizedJsonPathSegment` diff --git a/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/core/NormalizedJsonPathSegment.kt b/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/core/NormalizedJsonPathSegment.kt index 055f6ad..3b0cea6 100644 --- a/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/core/NormalizedJsonPathSegment.kt +++ b/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/core/NormalizedJsonPathSegment.kt @@ -77,7 +77,7 @@ sealed interface NormalizedJsonPathSegment { override val descriptor: SerialDescriptor get() = SerialDescriptor( original = JsonElement.serializer().descriptor, - serialName = JsonDistinguishableSerializer::class.qualifiedName!!, + serialName = JsonDistinguishableSerializer::class.simpleName!!, ) override fun serialize( @@ -92,7 +92,7 @@ sealed interface NormalizedJsonPathSegment { override fun deserialize(decoder: Decoder): NormalizedJsonPathSegment { require(decoder is JsonDecoder) { - "Expected decoder to be ${JsonDecoder::class.qualifiedName!!}, but was `$decoder`." + "Expected decoder to be ${JsonDecoder::class.simpleName!!}, but was `$decoder`." } val jsonElement = decoder.decodeJsonElement().jsonPrimitive return if (jsonElement.isString) { From ea57689bba49c14f86e4faffd92d7818a5f3da66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Wed, 22 Jul 2026 20:26:58 +0200 Subject: [PATCH 07/10] per-target timeouts --- .../AntlrCoroutineConcurrencyNativeTest.kt | 12 ++++----- .../jsonpath/ConcurrencyStressFixtures.kt | 26 ++++++++++++------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/jsonpath4k/src/nativeTest/kotlin/at/asitplus/jsonpath/AntlrCoroutineConcurrencyNativeTest.kt b/jsonpath4k/src/nativeTest/kotlin/at/asitplus/jsonpath/AntlrCoroutineConcurrencyNativeTest.kt index b593d13..4df02bd 100644 --- a/jsonpath4k/src/nativeTest/kotlin/at/asitplus/jsonpath/AntlrCoroutineConcurrencyNativeTest.kt +++ b/jsonpath4k/src/nativeTest/kotlin/at/asitplus/jsonpath/AntlrCoroutineConcurrencyNativeTest.kt @@ -7,13 +7,13 @@ import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout -import kotlin.time.Duration.Companion.seconds /** * Coroutine-fan-out sibling of [AntlrConcurrencyNativeTest]. * - * Instead of a bounded, dedicated thread pool, this fires **500 coroutines at once** with `launch` onto - * [Dispatchers.Default] (which is multi-threaded on Kotlin/Native). The enclosing `coroutineScope` suspends + * Instead of a bounded, dedicated thread pool, this fires **many coroutines at once** (see + * [coroutineStressProfile]) with `launch` onto [Dispatchers.Default] (multi-threaded on Kotlin/Native), gated so + * they start together. The enclosing `coroutineScope` suspends * until all of them finish (structured concurrency joins the children for us — no manual gate/`joinAll`), so * they slam the ANTLR-generated parser's process-global, mutable prediction state (decoded `ATN`, * `decisionToDFA`, `PredictionContextCache`) concurrently. @@ -74,7 +74,7 @@ val AntlrCoroutineConcurrencyNativeTest by matrixSuite { // 1) COLD DFA, SAME EXPRESSION: many coroutines compile the identical complex expression with no warmup. test("$COROUTINES coroutines compiling the same expression on a cold parser", testConfig = realConcurrency) { - withTimeout(120.seconds) { + withTimeout(coroutineStressProfile.timeout) { repeat(ROUNDS) { val expr = "$.store.book[?@.price < 10 && @.category == 'fiction'].title" coroutineStress("same-expr") { _, _ -> @@ -86,7 +86,7 @@ val AntlrCoroutineConcurrencyNativeTest by matrixSuite { // 2) MANY DISTINCT EXPRESSIONS: coroutines spread across many expressions -> churn across many decisions. test("$COROUTINES coroutines compiling many distinct expressions", testConfig = realConcurrency) { - withTimeout(120.seconds) { + withTimeout(coroutineStressProfile.timeout) { repeat(ROUNDS) { coroutineStress("distinct-expr") { worker, iteration -> val expr = compileOnlyExpressions[(worker + iteration) % compileOnlyExpressions.size] @@ -102,7 +102,7 @@ val AntlrCoroutineConcurrencyNativeTest by matrixSuite { val baseline: Map = bookstoreExpressions.associateWith { expr -> JsonPath(expr).query(bookStore).map { it.value }.toString() } - withTimeout(120.seconds) { + withTimeout(coroutineStressProfile.timeout) { repeat(ROUNDS) { coroutineStress("correctness") { worker, iteration -> val expr = bookstoreExpressions[(worker + iteration) % bookstoreExpressions.size] diff --git a/jsonpath4k/src/nativeTest/kotlin/at/asitplus/jsonpath/ConcurrencyStressFixtures.kt b/jsonpath4k/src/nativeTest/kotlin/at/asitplus/jsonpath/ConcurrencyStressFixtures.kt index 84c0372..e6dbea3 100644 --- a/jsonpath4k/src/nativeTest/kotlin/at/asitplus/jsonpath/ConcurrencyStressFixtures.kt +++ b/jsonpath4k/src/nativeTest/kotlin/at/asitplus/jsonpath/ConcurrencyStressFixtures.kt @@ -9,6 +9,8 @@ import kotlinx.serialization.json.JsonElement import kotlin.experimental.ExperimentalNativeApi import kotlin.native.OsFamily import kotlin.native.Platform +import kotlin.time.Duration +import kotlin.time.Duration.Companion.minutes /** * Shared fixtures for the ANTLR concurrency stress suites ([AntlrConcurrencyNativeTest] uses real OS threads; @@ -20,8 +22,12 @@ import kotlin.native.Platform /** Real (non-virtual-time) execution: real concurrency cannot run under TestBalloon's virtual-time `TestScope`. */ internal val realConcurrency: TestConfig = TestConfig.testScope(isEnabled = false) -/** How hard to hammer, per fan-out unit (OS thread or coroutine). */ -internal data class StressProfile(val fanOut: Int, val iterations: Int, val rounds: Int) +/** + * How hard to hammer, per fan-out unit (OS thread or coroutine). [timeout] is a per-test safety net against a + * genuine deadlock — it must comfortably exceed the *expected* runtime (fanOut × iterations × rounds compiles), + * not just a hang, so it scales with the workload. + */ +internal data class StressProfile(val fanOut: Int, val iterations: Int, val rounds: Int, val timeout: Duration) /** * Sizes the fan-out to the resources of a typical runner for the **current target** (via [Platform.osFamily]) — @@ -36,10 +42,10 @@ internal data class StressProfile(val fanOut: Int, val iterations: Int, val roun * and every Kotlin/Native `Worker` costs an ~8 MB stack) → scale right down so the sim job stays fast/safe. */ internal val threadStress: StressProfile = when (Platform.osFamily) { - OsFamily.LINUX -> StressProfile(fanOut = 256, iterations = 200, rounds = 4) - OsFamily.MACOSX -> StressProfile(fanOut = 128, iterations = 200, rounds = 4) - OsFamily.WINDOWS -> StressProfile(fanOut = 64, iterations = 150, rounds = 3) - else -> StressProfile(fanOut = 12, iterations = 60, rounds = 2) + OsFamily.LINUX -> StressProfile(fanOut = 256, iterations = 200, rounds = 4, timeout = 10.minutes) + OsFamily.MACOSX -> StressProfile(fanOut = 128, iterations = 200, rounds = 4, timeout = 10.minutes) + OsFamily.WINDOWS -> StressProfile(fanOut = 64, iterations = 150, rounds = 3, timeout = 5.minutes) + else -> StressProfile(fanOut = 12, iterations = 60, rounds = 2, timeout = 2.minutes) } /** @@ -47,10 +53,10 @@ internal val threadStress: StressProfile = when (Platform.osFamily) { * strong reproducer only on many-core hosts (LINUX/MACOSX) and a light smoke test on constrained simulators. */ internal val coroutineStressProfile: StressProfile = when (Platform.osFamily) { - OsFamily.LINUX -> StressProfile(fanOut = 20_000, iterations = 10, rounds = 4) - OsFamily.MACOSX -> StressProfile(fanOut = 10_000, iterations = 10, rounds = 4) - OsFamily.WINDOWS -> StressProfile(fanOut = 5_000, iterations = 10, rounds = 3) - else -> StressProfile(fanOut = 1_000, iterations = 8, rounds = 2) + OsFamily.LINUX -> StressProfile(fanOut = 20_000, iterations = 10, rounds = 2, timeout = 10.minutes) + OsFamily.MACOSX -> StressProfile(fanOut = 10_000, iterations = 10, rounds = 2, timeout = 10.minutes) + OsFamily.WINDOWS -> StressProfile(fanOut = 5_000, iterations = 10, rounds = 2, timeout = 5.minutes) + else -> StressProfile(fanOut = 1_000, iterations = 8, rounds = 2, timeout = 2.minutes) } // Non-trivial, valid expressions. Filters, function extensions, logical operators, slices and descendant From f149a5bf252d6382116bdf5ead85a72d5bff259b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Thu, 23 Jul 2026 06:06:20 +0200 Subject: [PATCH 08/10] fix ios Tests --- .github/workflows/test-ios.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-ios.yml b/.github/workflows/test-ios.yml index 71da868..fde2543 100644 --- a/.github/workflows/test-ios.yml +++ b/.github/workflows/test-ios.yml @@ -13,7 +13,7 @@ jobs: distribution: 'temurin' java-version: '17' - name: Build klibs - run: ./gradlew iosArm64MainKlibrary iosX64MainKlibrary + run: ./gradlew iosArm64MainKlibrary - name: Run tests run: ./gradlew iosSimulatorArm64Test - name: Test Report From 1c0bd7778ae38e78661a74d29791c22ec93ff37f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Mon, 27 Jul 2026 05:51:50 +0200 Subject: [PATCH 09/10] address PR feedback --- CHANGELOG.md | 2 +- .../src/commonMain/kotlin/at/asitplus/jsonpath/JsonPath.kt | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66722a4..730575b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ * JVM * Logging on other targets defaults to NOOP * Kotlin 2.4.0 -* Fix race condition thanks to latest ANTLR 1.0.13 +* Fix race condition thanks to latest ANTLR 1.0.12 * Remove deprecations * Change Serializer names to work on web targets too diff --git a/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/JsonPath.kt b/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/JsonPath.kt index 42d6d12..3a5c54e 100644 --- a/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/JsonPath.kt +++ b/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/JsonPath.kt @@ -1,6 +1,5 @@ package at.asitplus.jsonpath -//import io.github.aakira.napier.Napier import at.asitplus.jsonpath.core.JsonPathCompiler import at.asitplus.jsonpath.core.JsonPathFilterExpressionType import at.asitplus.jsonpath.core.JsonPathFunctionExtension From 39dc12f7e0f0ffa5dd5b5dee5cd86d03017b0124 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Mon, 27 Jul 2026 06:08:21 +0200 Subject: [PATCH 10/10] address PR feedback --- README.md | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 37385e9..707aa7a 100644 --- a/README.md +++ b/README.md @@ -17,14 +17,15 @@ This is a Kotlin Multiplatform Library for using Json Paths as specified in [RFC ## Architecture -This library was built for [Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.html) targeting JVM/Android and iOS. Other targets might work, but are -not tested or even built. +This library was built for [Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.html), supporting all KMP targets, except WASI. Notable features for multiplatform are: -- Use of [Napier](https://github.com/AAkira/Napier) as the logging framework for the default compiler instance -- Use of [Kotest](https://kotest.io/) for unit tests +- Use of [Napier](https://github.com/AAkira/Napier) for default compiler error logging on JVM/Android, iOS, macOS, tvOS, JS, and WasmJS. + - By default, no `Antilog` is registered. + - On watchOS, Linux, Windows (mingw), and Android Native, the default listener does not log, due to Napier not supporting those platforms; provide a custom error listener if logging is required on those platforms. - Use of [kotlinx-serialization](https://github.com/Kotlin/kotlinx.serialization) for serialization from/to JSON and to have JsonElement as evaluation target for JsonPathQuery +- Use of [TestBalloon](https://infix-de.github.io/testBalloon/latest/) with [TestBalloon Addons](https://github.com/a-sit-plus/testballoon-addons) for unit tests. ## Using the Library 1. Add JsonPath4K as a dependency in your project (`at.asitplus:jsonpath4k:$version`) @@ -47,10 +48,10 @@ val jsonValue = nodeList[0].value.jsonPrimitive val normalizedPath = nodeList[0].normalizedJsonPath ``` -## Function extensions +## Function Extensions This library supports the function extensions specified in [RFC9535](https://www.rfc-editor.org/rfc/rfc9535.html#name-function-extensions) by default. -### Custom function extensions +### Custom Function Extensions Custom function extensions can be added using `JsonPath.defaultFunctionExtensionRepository.addExtension`: ```kotlin // adding a logical type function extension with 1 parameter of type NodesType @@ -118,14 +119,14 @@ JsonPath.defaultFunctionExtensionRepository.addExtension("count") { } ``` -### Removing Function extensions +### Removing Function Extensions Function extensions can be removed from the default repository by setting the value of `JsonPath.defaultFunctionExtensionRepository` to a new repository. Existing functions can be preserved by exporting them using `JsonPath.defaultFunctionExtensionRepository.export()` and selectively importing them into the new repository. -### Testing custom function extensions +### Testing Custom Function Extensions In order to test custom function extensions without polluting the default function extension repository, it is recommended to make an export and use the resulting map to build a new function extension retriever. ```kotlin @@ -143,17 +144,22 @@ val jsonPath = JsonPath(jsonPathStatement, functionExtensionRetriever = testRetr jsonPath.query(buildJsonElement {}) ``` -## Error handeling -The default compiler uses Napier for reporting errors. -It is possible to implement a custom error listener by extending `AntlrJsonPathCompilerErrorListener` and setting a new default compiler: +## Error Handling +Invalid JSONPath expressions fail during `JsonPath` construction with a `JsonPathCompilerException`. The compiler's error listener only receives diagnostic details; it does not suppress or replace that exception. + +On JVM/Android, iOS, macOS, tvOS, JS, and WasmJS, the default listener sends diagnostics to Napier (provided the application has registered an `Antilog`). On watchOS, Linux, Windows (mingw), and Android Native, the default listener is a no-op. Compilation failures still throw on these platforms, but logging requires a custom implementation of `AntlrJsonPathCompilerErrorListener`. + +Pass a compiler with that listener to one `JsonPath`, or assign it as the global default: ```kotlin -JsonPath.defaultCompiler = AntlrJsonPathCompiler( - errorListener = object : AntlrJsonPathCompilerErrorListener { - //TODO: IMPLEMENT MEMBERS - }, -) +val compiler = AntlrJsonPathCompiler(errorListener = myErrorListener) +val jsonPath = JsonPath(jsonPathExpression, compiler = compiler) + +// Optional: use it for subsequent JsonPath instances that do not specify a compiler. +JsonPath.defaultCompiler = compiler ``` +Using a single, shared default works because operations only use an ANTLR cache as global state and this cache is guarded to enable concurrent access and mutation. + ## Contributing External contributions are greatly appreciated! Just be sure to observe the contribution guidelines (see [CONTRIBUTING.md](CONTRIBUTING.md)).