diff --git a/.ci-java-version b/.ci-java-version new file mode 100644 index 0000000..209e3ef --- /dev/null +++ b/.ci-java-version @@ -0,0 +1 @@ +20 diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8451248 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,20 @@ +# noinspection EditorConfigKeyCorrectness +[*.{kt,kts}] +ij_kotlin_allow_trailing_comma = true +ij_kotlin_allow_trailing_comma_on_call_site = true +ij_kotlin_imports_layout=*,java.**,javax.**,kotlin.**,^ +insert_final_newline = true +ktlint_code_style = intellij_idea +ktlint_experimental = enabled +# ktlint_standard_annotation = disabled +# ktlint_standard_class-signature = disabled +# ktlint_standard_comment-wrapping = disabled +# ktlint_standard_filename = disabled +# ktlint_standard_function-naming = disabled +# ktlint_standard_function-signature = disabled +# ktlint_standard_package-name = disabled +# ktlint_standard_property-naming = disabled +# ktlint_standard_spacing-between-declarations-with-annotations = disabled +# ktlint_standard_trailing-comma-on-call-site = disabled +# ktlint_standard_trailing-comma-on-declaration-site = disabled +max_line_length = 120 diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index d303b7b..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,12 +0,0 @@ -version: 2 - -updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "daily" - time: "08:00" - timezone: "America/New_York" - target-branch: "master" - commit-message: - prefix: "Dependabot" diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index fbead32..c987433 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -6,27 +6,44 @@ on: branches: [master] jobs: - check: + danger: runs-on: ubuntu-latest steps: - - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + - uses: actions/checkout@v4 - - uses: actions/setup-java@a18c333f3f14249953dab3e186e5e21bf3390f1d + - name: Danger + uses: docker://ghcr.io/danger/danger-kotlin:1.2.0 + with: + args: --failOnErrors --no-publish-check + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + check: + runs-on: macos-latest + steps: + - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 + + - uses: actions/setup-java@v3.12.0 with: distribution: 'zulu' - java-version: 17 + java-version-file: .ci-java-version - - uses: gradle/wrapper-validation-action@e6e38bacfdf1a337459f332974bb2327a31aaf4b + - uses: gradle/wrapper-validation-action@v1.1.0 - name: Setup Gradle - uses: gradle/gradle-build-action@fd32ae908111fe31afa48827bd1ee909540aa971 + uses: gradle/gradle-build-action@v2.8.0 with: gradle-version: wrapper - name: Run gradle check run: ./gradlew check - - uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 + - name: Run ktlint and detekt + run: ./format --no-format && ./gradlew detektAppleMain detektJvmMain detektJsMain detektMetadataMain -x wasmBrowserTest + + - uses: codecov/codecov-action@v3.1.4 with: files: build/reports/test/jacocoTestReport.xml fail_ci_if_error: true + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/publish_release.yml b/.github/workflows/publish_release.yml index e59caf6..9caeac1 100644 --- a/.github/workflows/publish_release.yml +++ b/.github/workflows/publish_release.yml @@ -1,38 +1,39 @@ name: Publish a release on: - push: - branches: - - release + workflow_dispatch: env: VERSION_FILE: gradle.properties VERSION_EXTRACT_PATTERN: '(?<=VERSION_NAME=).+' GH_USER_NAME: github.actor + GRADLE_OPTS: -Dorg.gradle.daemon=false -Dkotlin.incremental=false -Dorg.gradle.jvmargs="-Xmx4g -Xms512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:MaxMetaspaceSize=1024m" jobs: - publish_release: - runs-on: ubuntu-latest + publish_artifacts: + runs-on: macos-latest steps: - - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 + with: + token: ${{ secrets.PUSH_PAT }} - name: Generate versions - uses: HardNorth/github-version-generate@996e8c5b6ec9cd3e825caf5502d5176238f1109e + uses: HardNorth/github-version-generate@v.1.3.0 with: version-source: file version-file: ${{ env.VERSION_FILE }} version-file-extraction-pattern: ${{ env.VERSION_EXTRACT_PATTERN }} - - uses: actions/setup-java@a18c333f3f14249953dab3e186e5e21bf3390f1d + - uses: actions/setup-java@v3.12.0 with: distribution: 'zulu' - java-version: 17 + java-version-file: .ci-java-version - - uses: gradle/wrapper-validation-action@e6e38bacfdf1a337459f332974bb2327a31aaf4b + - uses: gradle/wrapper-validation-action@v1.1.0 - name: Setup Gradle - uses: gradle/gradle-build-action@fd32ae908111fe31afa48827bd1ee909540aa971 + uses: gradle/gradle-build-action@v2.8.0 with: gradle-version: wrapper @@ -41,24 +42,73 @@ jobs: ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.ARTIFACT_SIGNING_PRIVATE_KEY }} - ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.ARTIFACT_SIGNING_PRIVATE_KEY_PASSWORD }} - run: ./gradlew publish -PVERSION_NAME=${{ env.RELEASE_VERSION }} + run: ./gradlew publishAllPublicationsToMavenCentralRepository -Pversion=${{ env.RELEASE_VERSION }} + + publish_release: + needs: publish_artifacts + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 + with: + token: ${{ secrets.PUSH_PAT }} + + - uses: actions/setup-java@v3.12.0 + with: + distribution: 'zulu' + java-version-file: .ci-java-version + + - uses: gradle/wrapper-validation-action@v1.1.0 + + - name: Setup Gradle + uses: gradle/gradle-build-action@v2.8.0 + with: + gradle-version: wrapper + + - name: Generate versions + uses: HardNorth/github-version-generate@v1.3.0 + with: + version-source: file + version-file: ${{ env.VERSION_FILE }} + version-file-extraction-pattern: ${{ env.VERSION_EXTRACT_PATTERN }} + + - name: Create, checkout, and push release branch + run: | + git config user.name eygraber + git config user.email 1100381+eygraber@users.noreply.github.com + git checkout -b releases/${{ env.RELEASE_VERSION }} + git push origin releases/${{ env.RELEASE_VERSION }} + + - name: Import GPG Key + uses: crazy-max/ghaction-import-gpg@82a020f1f7f605c65dd2449b392a52c3fcfef7ef # v6.0.0 + with: + gpg_private_key: ${{ secrets.GIT_SIGNING_PRIVATE_KEY }} + passphrase: ${{ secrets.GIT_SIGNING_PRIVATE_KEY_PASSWORD }} + git_user_signingkey: true + git_commit_gpgsign: true + git_tag_gpgsign: true + + - name: Store SHA of HEAD commit on ENV + run: echo "GIT_HEAD=$(git rev-parse HEAD)" >> $GITHUB_ENV - name: Create tag id: create_tag - uses: actions/github-script@7dff1a87643417cf3b95bb10b29f4c4bc60d8ebd + uses: actions/github-script@v6 with: + github-token: ${{ secrets.PUSH_PAT }} script: | + const {GIT_HEAD} = process.env github.rest.git.createRef({ owner: context.repo.owner, repo: context.repo.repo, ref: "refs/tags/${{ env.RELEASE_VERSION }}", - sha: context.sha + sha: `${GIT_HEAD}` }) - name: Build changelog id: build_changelog - uses: mikepenz/release-changelog-builder-action@000e44613cdb6c340ac98cb1582f99e8d3230058 + uses: mikepenz/release-changelog-builder-action@v4 with: configuration: "changelog_config.json" toTag: ${{ env.RELEASE_VERSION }} @@ -67,23 +117,28 @@ jobs: - name: Create release id: create_release - uses: ncipollo/release-action@4c75f0f2e4ae5f3c807cf0904605408e319dcaac + uses: ncipollo/release-action@v1.13.0 with: body: ${{ steps.build_changelog.outputs.changelog }} - commit: release name: Release ${{ env.RELEASE_VERSION }} tag: ${{ env.RELEASE_VERSION }} - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ secrets.PUSH_PAT }} - - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + - uses: actions/checkout@v4 with: - ref: 'master' + ref: ${{ github.event.head_ref }} token: ${{ secrets.PUSH_PAT }} - name: Prepare next dev version + id: prepare_next_dev run: | - sed -i -e 's/${{ env.CURRENT_VERSION }}/${{ env.NEXT_VERSION }}/g' gradle.properties - sed -i -E -e 's/[0-9]+\.[0-9]+\.[0-9]+/${{ env.RELEASE_VERSION }}/g' README.md - git add gradle.properties - git commit -m "Prepare next dev version (${{ env.NEXT_VERSION }})" - git push origin master + sed -i -e 's/${{ env.CURRENT_VERSION }}/${{ env.NEXT_VERSION }}/g' gradle.properties && \ + sed -i -E -e 's/uri-kmp(:|\/)[0-9]+\.[0-9]+\.[0-9]+/uri-kmp\1${{ env.RELEASE_VERSION }}/g' README.md + + - name: Commit next dev version + id: commit_next_dev + uses: EndBug/add-and-commit@v9 + with: + add: "['gradle.properties', 'README.md']" + default_author: github_actions + message: "Prepare next dev version" diff --git a/.github/workflows/publish_snapshot.yml b/.github/workflows/publish_snapshot.yml index 1d68ec1..44de900 100644 --- a/.github/workflows/publish_snapshot.yml +++ b/.github/workflows/publish_snapshot.yml @@ -7,20 +7,20 @@ on: jobs: publish_snapshot: - runs-on: ubuntu-latest + runs-on: macos-latest steps: - - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 - - uses: actions/setup-java@a18c333f3f14249953dab3e186e5e21bf3390f1d + - uses: actions/setup-java@v3.12.0 with: distribution: 'zulu' - java-version: 17 + java-version-file: .ci-java-version - - uses: gradle/wrapper-validation-action@e6e38bacfdf1a337459f332974bb2327a31aaf4b + - uses: gradle/wrapper-validation-action@v1.1.0 - name: Setup Gradle - uses: gradle/gradle-build-action@fd32ae908111fe31afa48827bd1ee909540aa971 + uses: gradle/gradle-build-action@v2.8.0 with: gradle-version: wrapper @@ -29,4 +29,9 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} - run: ./gradlew publish + ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.ARTIFACT_SIGNING_PRIVATE_KEY }} + run: ./gradlew :uri:publishAllPublicationsToMavenCentralRepository + +env: + GRADLE_OPTS: -Dorg.gradle.daemon=false -Dkotlin.incremental=false -Dorg.gradle.jvmargs="-Xmx4g -Xms512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:MaxMetaspaceSize=1024m" + DEPENDENCY_GRAPH_INCLUDE_CONFIGURATIONS: runtimeClasspath|releaseRuntimeClasspath diff --git a/.gitignore b/.gitignore index f13187a..4178238 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ out/ .idea/ build/ local.properties +tmp diff --git a/Dangerfile.df.kts b/Dangerfile.df.kts new file mode 100644 index 0000000..512cf49 --- /dev/null +++ b/Dangerfile.df.kts @@ -0,0 +1,47 @@ +import systems.danger.kotlin.* +import java.util.Locale + +danger(args) { + with(github) { + val labelNames = issue.labels.map { it.name }.toSet() + + /* + # -------------------------------------------------------------------------------------------------------------------- + # Check if labels were added to the pull request + #-------------------------------------------------------------------------------------------------------------------- + */ + val labelsToFilter = setOf("hold", "skip release notes") + val acceptableLabels = labelNames.filter { it !in labelsToFilter } + + if(acceptableLabels.isEmpty() && pullRequest.head.ref != "bots/bump-version") { + fail("PR needs labels (hold and skip release notes don't count)") + } + + /* + # -------------------------------------------------------------------------------------------------------------------- + # Don't merge if there is a WIP or Hold label applied + # -------------------------------------------------------------------------------------------------------------------- + */ + if("Hold" in labelNames) fail("This PR cannot be merged with a hold label applied") + + /* + # -------------------------------------------------------------------------------------------------------------------- + # Check if merge commits were added to the pull request + # -------------------------------------------------------------------------------------------------------------------- + */ + val mergeCommitRegex = Regex("^Merge branch '${pullRequest.base.ref}'.*") + if(git.commits.any { it.message.matches(mergeCommitRegex) }) { + fail("Please rebase to get rid of the merge commits in this PR") + } + } + + /* + # -------------------------------------------------------------------------------------------------------------------- + # Make sure that no crash files or dumps are in the commit + # -------------------------------------------------------------------------------------------------------------------- + */ + val touchedFiles = git.createdFiles + git.modifiedFiles + if(touchedFiles.any { it.startsWith("hs_err_pid") || it.startsWith("java_pid") }) { + fail("Please remove any error logs (hs_err_pid*.log) or heap dumps (java_pid*.hprof)") + } +} diff --git a/LICENSE b/LICENSE index e1500f2..133c173 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ BSD 3-Clause License Copyright (c) 2019, Nikolay Feldman +Copyright (c) 2023, Eliezer Graber All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/README.md b/README.md index 1c16af6..0f1ecb3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # JsonPathKt [![Build Status](https://travis-ci.com/codeniko/JsonPathKt.svg?branch=master)](https://travis-ci.com/codeniko/JsonPathKt) -[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.nfeld.jsonpathkt/jsonpathkt/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.nfeld.jsonpathkt/jsonpathkt) +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.eygraber/jsonpathkt/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.eygraber/jsonpathkt) [![codecov](https://codecov.io/gh/codeniko/JsonPathKt/branch/master/graph/badge.svg)](https://codecov.io/gh/codeniko/JsonPathKt) **A lighter and more efficient implementation of JsonPath in Kotlin.** @@ -42,8 +42,8 @@ jsonpath.readFromJson>>(json1) jsonpath.readFromJson>>(json2) ``` -*JsonPathKt uses [Jackson](https://github.com/FasterXML/jackson) to deserialize JSON strings. `JsonPath.parse` returns a Jackson -`JsonNode` object, so if you've already deserialized, you can also `read` the jsonpath value directly.* +*JsonPathKt uses [kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) to deserialize JSON strings. `JsonPath.parse` returns a +`JsonElement` object, so if you've already deserialized, you can also `read` the jsonpath value directly.* ## Getting started @@ -67,14 +67,14 @@ dependencies { ## Accessor operators -| Operator | Description | -| :------------------------ | :----------------------------------------------------------------- | -| `$` | The root element to query. This begins all path expressions. | -| `..` | Deep scan for values behind followed key value accessor | -| `.` | Dot-notated key value accessor for JSON objects | -| `['' (, '')]` | Bracket-notated key value accessor for JSON objects, comma-delimited| -| `[ (, )]` | JSON array accessor for index or comma-delimited indices | -| `[start:end]` | JSON array range accessor from start (inclusive) to end (exclusive)| +| Operator | Description | +|:--------------------------|:---------------------------------------------------------------------| +| `$` | The root element to query. This begins all path expressions. | +| `..` | Deep scan for values behind followed key value accessor | +| `.` | Dot-notated key value accessor for JSON objects | +| `['' (, '')]` | Bracket-notated key value accessor for JSON objects, comma-delimited | +| `[ (, )]` | JSON array accessor for index or comma-delimited indices | +| `[start:end]` | JSON array range accessor from start (inclusive) to end (exclusive) | ## Path expression examples JsonPathKt expressions can use any combination of dot–notation and bracket–notation operators to access JSON values. For examples, these all evaluate to the same result: @@ -110,24 +110,24 @@ Given the JSON: } ``` -| JsonPath | Result | -| :------- | :----- | -| $.family | The family object | -| $.family.children | The children array | -| $.family['children'] | The children array | -| $.family.children[2] | The second child object | -| $.family.children[-1] | The last child object | -| $.family.children[-3] | The 3rd to last child object | -| $.family.children[1:3] | The 2nd and 3rd children objects | -| $.family.children[:3] | The first three children | -| $.family.children[:-1] | The first three children | -| $.family.children[2:] | The last two children | -| $.family.children[-2:] | The last two children | -| $..name | All names | -| $.family..name | All names nested within family object | -| $.family.children[:3]..age | The ages of first three children | -| $..['name','nickname'] | Names & nicknames (if any) of all children | -| $.family.children[0].* | Names & age values of first child | +| JsonPath | Result | +|:---------------------------|:-------------------------------------------| +| $.family | The family object | +| $.family.children | The children array | +| $.family['children'] | The children array | +| $.family.children[2] | The second child object | +| $.family.children[-1] | The last child object | +| $.family.children[-3] | The 3rd to last child object | +| $.family.children[1:3] | The 2nd and 3rd children objects | +| $.family.children[:3] | The first three children | +| $.family.children[:-1] | The first three children | +| $.family.children[2:] | The last two children | +| $.family.children[-2:] | The last two children | +| $..name | All names | +| $.family..name | All names nested within family object | +| $.family.children[:3]..age | The ages of first three children | +| $..['name','nickname'] | Names & nicknames (if any) of all children | +| $.family.children[0].* | Names & age values of first child | ## Benchmarks These are benchmark tests of JsonPathKt against Jayway's JsonPath implementation. Results for each test is the average of @@ -136,46 +136,38 @@ You can run these tests locally with `./runBenchmarks.sh` **Evaluating/reading path against large JSON** -| Path Tested | JsonPathKt (ms) | JsonPath (ms) | -| :---------- | :------ | :----- | -| $[0].friends[1].other.a.b['c'] | 88 ms *(35 ms w/ cache)* | 144 ms *(79 ms w/ cache)* | -| $[2]._id | 34 ms *(14 ms w/ cache)* | 48 ms *(28 ms w/ cache)* | -| $..name | 82 ms *(84 ms w/ cache)* | 450 ms *(572 ms w/ cache)* | -| $..['email','name'] | 116 ms *(108 ms w/ cache)* | 479 ms *(522 ms w/ cache)* | -| $..[1] | 203 ms *(202 ms w/ cache)* | 401 ms *(423 ms w/ cache)* | -| $..[:2] | 352 ms *(346 ms w/ cache)* | 442 ms *(437 ms w/ cache)* | -| $..[2:] | 426 ms *(419 ms w/ cache)* | 476 ms *(470 ms w/ cache)* | -| $[0]['tags'][-3] | 67 ms *(17 ms w/ cache)* | 83 ms *(37 ms w/ cache)* | -| $[0]['tags'][:3] | 90 ms *(36 ms w/ cache)* | 103 ms *(55 ms w/ cache)* | -| $[0]['tags'][3:] | 97 ms *(48 ms w/ cache)* | 112 ms *(65 ms w/ cache)* | -| $[0]['tags'][3:5] | 85 ms *(29 ms w/ cache)* | 101 ms *(50 ms w/ cache)* | -| $[0]['tags'][0,3,5] | 95 ms *(36 ms w/ cache)* | 122 ms *(52 ms w/ cache)* | -| $[0]['latitude','longitude','isActive'] | 97 ms *(36 ms w/ cache)* | 124 ms *(59 ms w/ cache)* | -| $[0]['tags'].* | 88 ms *(47 ms w/ cache)* | 121 ms *(86 ms w/ cache)* | -| $[0]..* | 828 ms *(796 ms w/ cache)* | 797 ms *(830 ms w/ cache)* | +| Path Tested | JsonPathKt (ms) | JsonPath (ms) | +|:----------------------------------------|:----------------|:--------------| +| $[0].friends[1].other.a.b['c'] | 31 ms | 81 ms | +| $[2]._id | 11 ms | 28 ms | +| $..name | 42 ms | 472 ms | +| $..['email','name'] | 55 ms | 479 ms | +| $..[1] | 40 ms | 412 ms | +| $..[:2] | 47 ms | 462 ms | +| $..[2:] | 69 ms | 443 ms | +| $[0]['tags'][-3] | 22 ms | 45 ms | +| $[0]['tags'][:3] | 29 ms | 62 ms | +| $[0]['tags'][3:] | 30 ms | 68 ms | +| $[0]['tags'][3:5] | 30 ms | 62 ms | +| $[0]['tags'][0,3,5] | 26 ms | 63 ms | +| $[0]['latitude','longitude','isActive'] | 31 ms | 96 ms | +| $[0]['tags'].* | 17 ms | 73 ms | +| $[0]..* | 85 ms | 597 ms | **Compiling JsonPath strings to internal tokens** -| Path size | JsonPathKt | JsonPath | -| :-------- | :----------- | :------- | -| 7 chars, 1 tokens | 14 ms *(2 ms w/ cache)* | 9 ms *(9 ms w/ cache)* | -| 16 chars, 3 tokens | 26 ms *(2 ms w/ cache)* | 25 ms *(25 ms w/ cache)* | -| 30 chars, 7 tokens | 55 ms *(2 ms w/ cache)* | 63 ms *(57 ms w/ cache)* | -| 65 chars, 16 tokens | 114 ms *(2 ms w/ cache)* | 157 ms *(142 ms w/ cache)* | -| 88 chars, 19 tokens | 205 ms *(2 ms w/ cache)* | 234 ms *(204 ms w/ cache)* | +| Path size | JsonPathKt | JsonPath | +|:--------------------|:-----------|:---------| +| 7 chars, 1 tokens | 5 ms | 5 ms | +| 16 chars, 3 tokens | 11 ms | 13 ms | +| 30 chars, 7 tokens | 21 ms | 32 ms | +| 65 chars, 16 tokens | 48 ms | 69 ms | +| 88 chars, 19 tokens | 66 ms | 103 ms | # Cache -JsonPathKt uses an LRU cache by default to cache compiled JsonPath tokens. If you don't want to use the cache, you can disable it or set the CacheProvider to use your own implementation of the Cache interface. -```kotlin -// Disable cache -CacheProvider.setCache(null) - -// Implement your own cache -CacheProvider.setCache(object : Cache { - override fun get(path: String): JsonPath? { ... } - override fun put(path: String, jsonPath: JsonPath) { ... } -}) +JsonPathKt doesn't provide a caching layer anymore. If caching is desired, there are multiple +KMP caching libraries that can be used to wrap JsonPathKt. ``` [![Analytics](https://ga-beacon.appspot.com/UA-116910991-3/jsonpathlite/index)](https://github.com/igrigorik/ga-beacon) diff --git a/build.gradle.kts b/build.gradle.kts index 037608e..83b2092 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,159 +1,24 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import com.eygraber.conventions.tasks.deleteRootBuildDirWhenCleaning +import org.jetbrains.kotlin.gradle.dsl.JvmTarget -plugins { - kotlin("jvm") version "1.7.10" - jacoco - id("com.vanniktech.maven.publish") version "0.22.0" -} - -val jacksonVersion = "2.13.1" - -repositories { - mavenCentral() -} - -dependencies { - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") - api("com.fasterxml.jackson.core:jackson-core:$jacksonVersion") - api("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") - implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion") -} - -tasks.withType { - kotlinOptions.jvmTarget = "1.8" -} - -// pass -PreadmeFormat to format benchmark results to update readme -val readmeFormat = hasProperty("readmeFormat") - -val kotestVersion = "4.6.4" - -dependencies { - testImplementation("org.junit.jupiter:junit-jupiter:5.6.2") - testApi("com.jayway.jsonpath:json-path:2.4.0") - testImplementation("org.json:json:20180813") - testImplementation("io.kotest:kotest-runner-junit5-jvm:$kotestVersion") // for kotest framework - testImplementation("io.kotest:kotest-assertions-core-jvm:$kotestVersion") // for kotest core jvm assertions -} - -tasks.register("benchmark") { - useJUnitPlatform() - - if(readmeFormat) { - jvmArgs("-DreadmeFormat=true") - } - - filter { - include("com/nfeld/jsonpathkt/BenchmarkTest.class") - } - - testLogging { - showStandardStreams = true - } - - // Make this task never up to date, thus forcing rerun of all tests whenever task is run - outputs.upToDateWhen { false } -} - -tasks.register("perfTest") { - useJUnitPlatform() - - filter { - include("com/nfeld/jsonpathkt/PerfTest.class") - } - - testLogging { - // show test results for following events - events("PASSED", "FAILED", "SKIPPED") - - // show printlines - showStandardStreams = true +buildscript { + dependencies { + classpath(libs.buildscript.detekt) + classpath(libs.buildscript.dokka) + classpath(libs.buildscript.kotlin) + classpath(libs.buildscript.publish) } - - // Make this task never up to date, thus forcing rerun of all tests whenever task is run - outputs.upToDateWhen { false } } -tasks.test { - useJUnitPlatform() - - filter { - exclude( - "com/nfeld/jsonpathkt/BenchmarkTest.class", - "com/nfeld/jsonpathkt/PerfTest.class" - ) - } - - testLogging { - // show test results for following events - events("PASSED", "FAILED", "SKIPPED") - - // show printlines - showStandardStreams = true - } - - addTestListener(object : TestListener { - override fun beforeSuite(suite: TestDescriptor?) {} - override fun afterTest(testDescriptor: TestDescriptor?, result: TestResult?) {} - override fun beforeTest(testDescriptor: TestDescriptor?) {} - - override fun afterSuite(suite: TestDescriptor?, result: TestResult?) { - val parent = suite?.parent - if(parent != null && result != null) { - println("\nTest result: ${result.resultType}") - println( - """ - |Test summary: ${result.testCount} tests, - | ${result.successfulTestCount} succeeded, - | ${result.failedTestCount} failed, - | ${result.skippedTestCount} skipped - """.trimMargin().replace("\n", "") - ) - } - } - - }) - - configure { - setDestinationFile(layout.buildDirectory.file("jacoco/junitPlatformTest.exec").map { it.asFile }) - isIncludeNoLocationClasses = true - excludes = listOf( - "*/LRUCache\$LRUMap*", - "*/JsonNodeKt*", - "jdk.internal.*" - ) - } - - // Make this task never up to date, thus forcing rerun of all tests whenever task is run - outputs.upToDateWhen { false } -} - -jacoco { - toolVersion = "0.8.8" - reportsDirectory.set(layout.buildDirectory.dir("reports")) +plugins { + base + alias(libs.plugins.conventions) } -tasks.jacocoTestReport.configure { - reports { - xml.required.set(true) - html.required.set(true) - csv.required.set(false) - } -} +deleteRootBuildDirWhenCleaning() -tasks.jacocoTestCoverageVerification { - violationRules { - rule { - limit { - counter = "LINE" - value = "COVEREDRATIO" - minimum = BigDecimal(0.85) - } - } +gradleConventionsDefaults { + kotlin { + jvmTargetVersion = JvmTarget.JVM_11 } } - -tasks.check.configure { - dependsOn(tasks.jacocoTestReport) - dependsOn(tasks.jacocoTestCoverageVerification) -} diff --git a/changelog_config.json b/changelog_config.json index 4b0f90d..1273aa5 100644 --- a/changelog_config.json +++ b/changelog_config.json @@ -1,9 +1,13 @@ { "categories": [ { - "title": "## 🚀 Enhancements", + "title": "## ✨ Enhancements", "labels": ["enhancement"] }, + { + "title": "## 🚀 Features", + "labels": ["feature"] + }, { "title": "## ⚙️ Chores", "labels": ["chore"] @@ -21,7 +25,7 @@ "labels": ["documentation"] }, { - "title": "## \uD83D\uDCE6 Dependencies", + "title": "\uD83D\uDCE6 Dependencies", "labels": ["dependencies"] }, { @@ -34,7 +38,7 @@ } ], "ignore_labels": [ - "duplicate", "good first issue", "help wanted", "invalid", "question", "wontfix", "skip release notes", "hold" + "duplicate", "good first issue", "help wanted", "invalid", "question", "wontfix" ], "sort": "ASC", "template": "${{CHANGELOG}}", diff --git a/detekt.yml b/detekt.yml new file mode 100644 index 0000000..af26233 --- /dev/null +++ b/detekt.yml @@ -0,0 +1,574 @@ +build: + maxIssues: 0 + excludeCorrectable: false + weights: + # complexity: 2 + # LongParameterList: 1 + # style: 1 + # comments: 1 + +config: + validation: true + warningsAsErrors: true + +processors: + active: true + exclude: [] + # - 'FunctionCountProcessor' + # - 'PropertyCountProcessor' + # - 'ClassCountProcessor' + # - 'PackageCountProcessor' + # - 'KtFileCountProcessor' + +console-reports: + active: true + exclude: + - 'ProjectStatisticsReport' + - 'ComplexityReport' + - 'NotificationReport' + # - 'FindingsReport' + - 'FileBasedFindingsReport' + +comments: + active: false + AbsentOrWrongFileLicense: + active: false + licenseTemplateFile: 'license.template' + licenseTemplateIsRegex: false + CommentOverPrivateFunction: + active: false + CommentOverPrivateProperty: + active: false + DeprecatedBlockTag: + active: false + EndOfSentenceFormat: + active: false + endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)' + UndocumentedPublicClass: + active: false + searchInNestedClass: true + searchInInnerClass: true + searchInInnerObject: true + searchInInnerInterface: true + UndocumentedPublicFunction: + active: false + UndocumentedPublicProperty: + active: false + +complexity: + active: true + ComplexCondition: + active: true + threshold: 4 + ComplexInterface: + active: false + threshold: 10 + includeStaticDeclarations: false + includePrivateDeclarations: false + CyclomaticComplexMethod: + active: false + threshold: 15 + ignoreSingleWhenExpression: false + ignoreSimpleWhenEntries: false + ignoreNestingFunctions: false + nestingFunctions: ['run', 'let', 'apply', 'with', 'also', 'use', 'forEach', 'isNotNull', 'ifNull'] + LabeledExpression: + active: true + ignoredLabels: ['lazy'] + LargeClass: + active: false + threshold: 600 + LongMethod: + active: false + threshold: 60 + LongParameterList: + active: false + functionThreshold: 6 + constructorThreshold: 7 + ignoreDefaultParameters: false + ignoreDataClasses: true + ignoreAnnotated: [] + MethodOverloading: + active: true + threshold: 6 + NamedArguments: + active: false + threshold: 3 + NestedBlockDepth: + active: false + threshold: 6 + ReplaceSafeCallChainWithRun: + active: false + StringLiteralDuplication: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + threshold: 3 + ignoreAnnotation: true + excludeStringsWithLessThan5Characters: true + ignoreStringsRegex: '$^' + TooManyFunctions: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + thresholdInFiles: 11 + thresholdInClasses: 11 + thresholdInInterfaces: 11 + thresholdInObjects: 11 + thresholdInEnums: 11 + ignoreDeprecated: false + ignorePrivate: false + ignoreOverridden: false + +coroutines: + active: true + GlobalCoroutineUsage: + active: false + InjectDispatcher: + active: false + RedundantSuspendModifier: + active: true + SleepInsteadOfDelay: + active: true + SuspendFunWithFlowReturnType: + active: true + +empty-blocks: + active: true + EmptyCatchBlock: + active: true + allowedExceptionNameRegex: '_|(ignore|expected).*' + EmptyClassBlock: + active: true + EmptyDefaultConstructor: + active: true + EmptyDoWhileBlock: + active: true + EmptyElseBlock: + active: true + EmptyFinallyBlock: + active: true + EmptyForBlock: + active: true + EmptyFunctionBlock: + active: false + ignoreOverridden: false + EmptyIfBlock: + active: true + EmptyInitBlock: + active: true + EmptyKtFile: + active: true + EmptySecondaryConstructor: + active: true + EmptyTryBlock: + active: true + EmptyWhenBlock: + active: true + EmptyWhileBlock: + active: true + +exceptions: + active: true + ExceptionRaisedInUnexpectedLocation: + active: true + methodNames: ['toString', 'hashCode', 'equals', 'finalize'] + InstanceOfCheckForException: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + NotImplementedDeclaration: + active: true + ObjectExtendsThrowable: + active: false + PrintStackTrace: + active: false + RethrowCaughtException: + active: true + ReturnFromFinally: + active: true + ignoreLabeled: false + SwallowedException: + active: false + ignoredExceptionTypes: + - InterruptedException + - NumberFormatException + - ParseException + - MalformedURLException + allowedExceptionNameRegex: '_|(ignore|expected).*' + ThrowingExceptionFromFinally: + active: false + ThrowingExceptionInMain: + active: false + ThrowingExceptionsWithoutMessageOrCause: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + exceptions: + - IllegalArgumentException + - IllegalStateException + - IOException + ThrowingNewInstanceOfSameException: + active: true + TooGenericExceptionCaught: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + exceptionNames: + - ArrayIndexOutOfBoundsException + - Error + - Exception + - IllegalMonitorStateException + - NullPointerException + - IndexOutOfBoundsException + - RuntimeException + - Throwable + allowedExceptionNameRegex: '_|(ignore|expected).*' + TooGenericExceptionThrown: + active: false + exceptionNames: + - Error + - Exception + - Throwable + - RuntimeException + +naming: + active: true + ClassNaming: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + classPattern: '[A-Z][a-zA-Z0-9]*' + ConstructorParameterNaming: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + parameterPattern: '_?[a-z][A-Za-z0-9]*' + privateParameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + EnumNaming: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + enumEntryPattern: '([A-Z][a-z0-9]+)((\d)|([A-Z0-9][a-z0-9]+))*([A-Z])?' + ForbiddenClassName: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + forbiddenName: [] + FunctionMaxLength: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + maximumFunctionNameLength: 30 + FunctionMinLength: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + minimumFunctionNameLength: 3 + FunctionNaming: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + functionPattern: '^([[a-z][A-Z]$][a-zA-Z$0-9]*)|(`.*`)$' + excludeClassPattern: '$^' + ignoreAnnotated: ['Composable'] + FunctionParameterNaming: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + parameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + InvalidPackageDeclaration: + active: true + excludes: ['**/*.kts'] + rootPackage: '' + MatchingDeclarationName: + active: false + mustBeFirst: true + MemberNameEqualsClassName: + active: true + NoNameShadowing: + active: true + NonBooleanPropertyPrefixedWithIs: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + ObjectPropertyNaming: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + constantPattern: '[A-Z][_A-Z0-9]*' + propertyPattern: '[A-Za-z][A-Za-z0-9]*' + privatePropertyPattern: '(_)?[A-Za-z][A-Za-z0-9]*' + PackageNaming: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*' + TopLevelPropertyNaming: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + constantPattern: '[A-Z][_A-Z0-9]*' + propertyPattern: '[A-Za-z][A-Za-z0-9]*' + privatePropertyPattern: '_?[A-Za-z][A-Za-z0-9]*' + VariableMaxLength: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + maximumVariableNameLength: 64 + VariableMinLength: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + minimumVariableNameLength: 1 + VariableNaming: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + variablePattern: '[a-z][A-Za-z0-9]*' + privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + +performance: + active: true + ArrayPrimitive: + active: true + ForEachOnRange: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + SpreadOperator: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + UnnecessaryTemporaryInstantiation: + active: true + +potential-bugs: + active: true + CastToNullableType: + active: false + Deprecation: + active: true + DontDowncastCollectionTypes: + active: false + DoubleMutabilityForCollection: + active: false + ElseCaseInsteadOfExhaustiveWhen: + active: false + EqualsAlwaysReturnsTrueOrFalse: + active: false + EqualsWithHashCodeExist: + active: true + ExitOutsideMain: + active: false + ExplicitGarbageCollectionCall: + active: true + HasPlatformType: + active: false + IgnoredReturnValue: + active: false + restrictToConfig: true + returnValueAnnotations: ['*.CheckReturnValue', '*.CheckResult'] + ImplicitDefaultLocale: + active: true + ImplicitUnitReturnType: + active: false + allowExplicitReturnType: true + InvalidRange: + active: true + IteratorHasNextCallsNextMethod: + active: true + IteratorNotThrowingNoSuchElementException: + active: true + LateinitUsage: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + ignoreAnnotated: [] + ignoreOnClassesPattern: '' + MapGetWithNotNullAssertionOperator: + active: false + NullCheckOnMutableProperty: + active: true + NullableToStringCall: + active: false + UnconditionalJumpStatementInLoop: + active: true + UnnecessaryNotNullOperator: + active: true + UnnecessarySafeCall: + active: true + UnreachableCatchBlock: + active: false + UnreachableCode: + active: true + UnsafeCallOnNullableType: + active: true + UnsafeCast: + active: false + UnusedUnaryOperator: + active: false + UselessPostfixExpression: + active: true + WrongEqualsTypeParameter: + active: true + +style: + active: true + BracesOnIfStatements: + active: true + CanBeNonNullable: + active: true + ClassOrdering: + active: false + CollapsibleIfStatements: + active: false + DataClassContainsFunctions: + active: false + conversionFunctionPrefix: ['to'] + DataClassShouldBeImmutable: + active: true + DestructuringDeclarationWithTooManyEntries: + active: false + maxDestructuringEntries: 3 + EqualsNullCall: + active: true + EqualsOnSignatureLine: + active: false + ExplicitCollectionElementAccessMethod: + active: true + ExplicitItLambdaParameter: + active: true + ExpressionBodySyntax: + active: true + includeLineWrapping: true + ForbiddenComment: + active: true + comments: ['FIXME', 'STOPSHIP'] + ForbiddenImport: + active: false + imports: [] + forbiddenPatterns: '' + ForbiddenMethodCall: + active: false + methods: ['kotlin.io.println', 'kotlin.io.print'] + ForbiddenVoid: + active: true + ignoreOverridden: true + ignoreUsageInGenerics: true + FunctionOnlyReturningConstant: + active: false + ignoreOverridableFunction: true + ignoreActualFunction: true + excludedFunctions: ['describeContents'] + ignoreAnnotated: ['dagger.Provides'] + LoopWithTooManyJumpStatements: + active: false + maxJumpCount: 1 + MagicNumber: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + ignoreNumbers: ['-1', '0', '1', '2'] + ignoreHashCodeFunction: true + ignorePropertyDeclaration: false + ignoreLocalVariableDeclaration: false + ignoreConstantDeclaration: true + ignoreCompanionObjectPropertyDeclaration: true + ignoreAnnotation: false + ignoreNamedArgument: true + ignoreEnums: false + ignoreRanges: false + ignoreExtensionFunctions: true + MandatoryBracesLoops: + active: true + MaxLineLength: + active: false # handled by ktlint + maxLineLength: 120 + MayBeConst: + active: true + ModifierOrder: + active: true + MultilineLambdaItParameter: + active: true + NestedClassesVisibility: + active: true + NewLineAtEndOfFile: + active: true + NoTabs: + active: true + ObjectLiteralToLambda: + active: true + OptionalAbstractKeyword: + active: true + OptionalUnit: + active: true + PreferToOverPairSyntax: + active: true + ProtectedMemberInFinalClass: + active: true + RedundantExplicitType: + active: true + RedundantHigherOrderMapUsage: + active: false + RedundantVisibilityModifierRule: + active: false + ReturnCount: + active: true + max: 3 + excludedFunctions: ['equals'] + excludeLabeled: false + excludeReturnFromLambda: true + excludeGuardClauses: true + SafeCast: + active: true + SerialVersionUIDInSerializableClass: + active: true + SpacingBetweenPackageAndImports: + active: true + ThrowsCount: + active: true + max: 3 + # excludeGuardClauses: true + TrailingWhitespace: + active: true + UnderscoresInNumericLiterals: + active: true + acceptableLength: 4 + UnnecessaryAbstractClass: + active: false + ignoreAnnotated: ['dagger.Module'] + UnnecessaryAnnotationUseSiteTarget: + active: true + UnnecessaryApply: + active: true + UnnecessaryFilter: + active: false + UnnecessaryInheritance: + active: true + UnnecessaryInnerClass: + active: true + UnnecessaryLet: + active: false + UnnecessaryParentheses: + active: true + UntilInsteadOfRangeTo: + active: true + UnusedImports: + active: true + UnusedPrivateClass: + active: true + UnusedPrivateMember: + active: false + allowedNames: '(_|ignored|expected|serialVersionUID)' + UseArrayLiteralsInAnnotations: + active: true + UseCheckOrError: + active: true + UseDataClass: + active: false + ignoreAnnotated: [] + allowVars: false + UseEmptyCounterpart: + active: true + UseIfEmptyOrIfBlank: + active: true + UseIfInsteadOfWhen: + active: false + UseIsNullOrEmpty: + active: true + UseOrEmpty: + active: true + UseRequire: + active: true + UseRequireNotNull: + active: true + UselessCallOnNotNull: + active: true + UtilityClassWithPublicConstructor: + active: true + VarCouldBeVal: + active: true + WildcardImport: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + excludeImports: [] diff --git a/format b/format new file mode 100755 index 0000000..5c48f52 --- /dev/null +++ b/format @@ -0,0 +1,53 @@ +#!/bin/bash + +version=$(sed -n 's/ktlint = "\(.*\)"/\1/p' gradle/libs.versions.toml) +url="https://github.com/pinterest/ktlint/releases/download/$version/ktlint" + +# Set the destination directory and file name +destination_dir="tmp" +file_name="ktlint-$version" + +mkdir -p $destination_dir + +# setting nullglob ensures proper behavior if nothing matches the glob +shopt -s nullglob +for file in $destination_dir/ktlint-*; do + if [[ "$file" != "$destination_dir/$file_name" ]]; then + rm "$file" + fi +done +shopt -u nullglob + +# Check if the file already exists in the destination directory +if [ ! -e "$destination_dir/$file_name" ]; then + if command -v curl >/dev/null 2>&1; then + curl -LJO "$url" + mv "ktlint" "$destination_dir/$file_name" + elif command -v wget >/dev/null 2>&1; then + wget -O "$destination_dir/$file_name" "$url" + else + echo "Error: curl or wget not found. Please install either curl or wget." + exit 1 + fi + + chmod +x "$destination_dir/$file_name" +fi + +should_format=true +for arg in "$@"; do + if [ "$arg" == "--no-format" ]; then + should_format=false + set -- "${@//--no-format/}" + break + fi +done + +args=() + +if [ "$should_format" = true ]; then + args+=("--format") +fi + +args+=("$@") + +"$destination_dir/$file_name" **/*.kt **/*.kts \!**/build/** \!Dangerfile.df.kts --color --color-name=YELLOW "${args[@]}" diff --git a/gradle.properties b/gradle.properties index cd66434..5c03052 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,25 +1,17 @@ kotlin.code.style=official -SONATYPE_AUTOMATIC_RELEASE=true -SONATYPE_HOST=DEFAULT -RELEASE_SIGNING_ENABLED=true +group=com.eygraber +version=2.0.1-SNAPSHOT -GROUP=com.nfeld.jsonpathkt -POM_ARTIFACT_ID=jsonpathkt -VERSION_NAME=2.0.1-SNAPSHOT - -POM_NAME=JsonPathKt -POM_DESCRIPTION=A lighter and more efficient implementation of JsonPath in Kotlin -POM_URL=https://github.com/codeniko/JsonPathKt +POM_URL=https://github.com/eygraber/JsonPathKt/ +POM_SCM_URL=https://github.com/eygraber/JsonPathKt/ +POM_SCM_CONNECTION=scm:git:git://github.com/eygraber/JsonPathKt.git +POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/eygraber/JsonPathKt.git POM_LICENSE_NAME=BSD-3-Clause -POM_LICENSE_URL=https://github.com/codeniko/JsonPathKt/blob/master/LICENSE +POM_LICENSE_URL=https://github.com/eygraber/JsonPathKt/blob/master/LICENSE POM_LICENSE_DIST=repo -POM_SCM_URL=https://github.com/codeniko/JsonPathKt -POM_SCM_CONNECTION=scm:git:git://github.com/codeniko/JsonPathKt.git -POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com:codeniko/JsonPathKt.git - POM_DEVELOPER_ID=codeniko POM_DEVELOPER_NAME=Nikolay Feldman -POM_DEVELOPER_URL=https://www.nfeld.com \ No newline at end of file +POM_DEVELOPER_URL=https://www.nfeld.com diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..48e1b01 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,44 @@ +[versions] +conventions = "0.0.49" + +detekt = "1.23.1" + +dokka = "1.9.0" + +jackson = "2.15.2" + +kotlin = "1.9.10" + +kotest = "5.7.2" + +ktlint = "1.0.0" + +publish = "0.25.3" + +[plugins] +conventions = { id = "com.eygraber.conventions", version.ref = "conventions" } +kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } + +[libraries] +buildscript-detekt = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" } +buildscript-dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" } +buildscript-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } +buildscript-publish = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "publish" } + +jackson-core = { module = "com.fasterxml.jackson.core:jackson-core", version.ref = "jackson" } +jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" } +jackson-moduleKotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson" } + +jayway-jsonPath = "com.jayway.jsonpath:json-path:2.8.0" + +json-org = "org.json:json:20230618" + +kotlinx-serialization-json = "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0" + +# not actually used; just here so renovate picks it up +ktlint = { module = "com.pinterest.ktlint:ktlint-bom", version.ref = "ktlint" } + +slf4j = "org.slf4j:slf4j-nop:2.0.7" + +test-junit = "org.junit.jupiter:junit-jupiter:5.10.0" +test-kotest-assertions = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 28861d2..7f93135 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 02e3011..ac72c34 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ -#Mon Jul 01 21:44:34 PDT 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip diff --git a/gradlew b/gradlew index cccdd3d..0adc8e1 100755 --- a/gradlew +++ b/gradlew @@ -1,78 +1,127 @@ -#!/usr/bin/env sh +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -81,92 +130,120 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +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 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# 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" ) -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=$((i+1)) + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" +# 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. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" fi +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index e95643d..6689b85 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,4 +1,20 @@ -@if "%DEBUG%" == "" @echo off +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -9,19 +25,23 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -35,7 +55,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -45,38 +65,26 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :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 %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/jsonpath/build.gradle.kts b/jsonpath/build.gradle.kts new file mode 100644 index 0000000..e42f8ab --- /dev/null +++ b/jsonpath/build.gradle.kts @@ -0,0 +1,189 @@ +import org.jetbrains.kotlin.gradle.plugin.mpp.AbstractKotlinNativeTargetPreset +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJvmCompilation +import org.jetbrains.kotlin.konan.target.KonanTarget + +plugins { + java + jacoco + id("com.eygraber.conventions-kotlin-multiplatform") + id("com.eygraber.conventions-detekt") + id("com.eygraber.conventions-publish-maven-central") + alias(libs.plugins.kotlinx.serialization) +} + +kotlin { + targets { + kmpTargets( + project = project, + android = false, + jvm = true, + ios = true, + macos = true, + wasm = false, + js = true + ) + + presets.withType>().forEach { + if(!it.konanTarget.family.isAppleFamily && it.konanTarget !in KonanTarget.deprecatedTargets) { + targetFromPreset(it) + } + } + + jvm { + compilations.registerBenchmarkCompilation() + } + } + + sourceSets { + commonMain { + dependencies { + implementation(libs.kotlinx.serialization.json) + } + } + + getByName("jvmBenchmark") { + dependsOn(commonMain.get()) + } + + getByName("commonTest") { + dependencies { + implementation(libs.test.kotest.assertions) + implementation(kotlin("test")) + } + } + } +} + +tasks.withType { + testLogging { + // show test results for following events + events("PASSED", "FAILED", "SKIPPED") + + // show printlines + showStandardStreams = true + } + + addTestListener( + object : TestListener { + override fun beforeSuite(suite: TestDescriptor?) {} + override fun afterTest(testDescriptor: TestDescriptor?, result: TestResult?) {} + override fun beforeTest(testDescriptor: TestDescriptor?) {} + + override fun afterSuite(suite: TestDescriptor?, result: TestResult?) { + val parent = suite?.parent + if (parent != null && result != null) { + println("\nTest result: ${result.resultType}") + println( + """ + |Test summary: ${result.testCount} tests, + | ${result.successfulTestCount} succeeded, + | ${result.failedTestCount} failed, + | ${result.skippedTestCount} skipped + """.trimMargin().replace("\n", "") + ) + } + } + + } + ) + + configure { + setDestinationFile( + layout.buildDirectory.file("jacoco/junitPlatformTest.exec").map { it.asFile }) + isIncludeNoLocationClasses = true + excludes = listOf( + "*/LRUCache\$LRUMap*", + "*/JsonNodeKt*", + "jdk.internal.*" + ) + } + + // Make this task never up to date, thus forcing rerun of all tests whenever task is run + outputs.upToDateWhen { false } +} + +jacoco { + toolVersion = "0.8.10" + reportsDirectory.set(rootProject.layout.buildDirectory.dir("reports")) +} + +tasks.jacocoTestReport.configure { + dependsOn(tasks.named("jvmTest")) + + val buildDirPath = layout.buildDirectory.asFile.get().absolutePath + + val coverageSourceDirs = listOf("src/commonMain") + val classFiles = File("$buildDirPath/classes/kotlin/jvm").walkBottomUp().toSet() + + classDirectories.setFrom(classFiles) + sourceDirectories.setFrom(files(coverageSourceDirs)) + + executionData.setFrom(files("$buildDirPath/jacoco/junitPlatformTest.exec")) + + reports { + xml.required.set(true) + html.required.set(true) + csv.required.set(false) + } +} + +tasks.jacocoTestCoverageVerification { + dependsOn(tasks.named("jvmTest")) + violationRules { + rule { + limit { + counter = "LINE" + value = "COVEREDRATIO" + minimum = BigDecimal(0.90) + } + } + } +} + +tasks.check.configure { + dependsOn(tasks.jacocoTestReport) + dependsOn(tasks.jacocoTestCoverageVerification) +} + +fun NamedDomainObjectContainer.registerBenchmarkCompilation() { + val main = getByName("main") + val test = getByName("test") + create("benchmark") { + defaultSourceSet { + dependencies { + implementation(main.compileDependencyFiles + main.output.classesDirs) + implementation(test.compileDependencyFiles + test.output.classesDirs) + + implementation(libs.jayway.jsonPath) + implementation(libs.json.org) + implementation(libs.test.junit) + implementation(libs.jackson.core) + implementation(libs.jackson.databind) + implementation(libs.jackson.moduleKotlin) + implementation(libs.slf4j) + + } + } + + // pass -PreadmeFormat to format benchmark results to update readme + val readmeFormat = hasProperty("readmeFormat") + + tasks.register("benchmark") { + classpath = compileDependencyFiles + runtimeDependencyFiles + output.allOutputs + testClassesDirs = output.classesDirs + + useJUnitPlatform() + + if (readmeFormat) { + jvmArgs("-DreadmeFormat=true") + } + + testLogging { + showStandardStreams = true + } + + // Make this task never up to date, thus forcing rerun of all tests whenever task is run + outputs.upToDateWhen { false } + } + } +} diff --git a/jsonpath/gradle.properties b/jsonpath/gradle.properties new file mode 100644 index 0000000..b8273f9 --- /dev/null +++ b/jsonpath/gradle.properties @@ -0,0 +1,3 @@ +POM_ARTIFACT_ID=jsonpathkt +POM_NAME=JsonPathKt +POM_DESCRIPTION=A lighter and more efficient implementation of JsonPath in Kotlin Multiplatform diff --git a/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/JsonNode.kt b/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/JsonNode.kt new file mode 100644 index 0000000..bc9fa3c --- /dev/null +++ b/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/JsonNode.kt @@ -0,0 +1,14 @@ +package com.nfeld.jsonpathkt + +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull + +data class JsonNode( + val element: JsonElement, + val isNewRoot: Boolean +) { + init { + require(!isNewRoot || element is JsonArray || element is JsonNull) + } +} diff --git a/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/JsonPath.kt b/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/JsonPath.kt new file mode 100644 index 0000000..9a80ceb --- /dev/null +++ b/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/JsonPath.kt @@ -0,0 +1,79 @@ +package com.nfeld.jsonpathkt + +import com.nfeld.jsonpathkt.extension.isNotNullOrMissing +import com.nfeld.jsonpathkt.tokens.Token +import kotlinx.serialization.KSerializer +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.contentOrNull +import kotlin.jvm.JvmStatic + +class JsonPath(path: String) { + private val path: String + val tokens: List + + /** + * Trim given path string and compile it on initialization + */ + init { + this.path = path.trim() + + tokens = PathCompiler.compile(this.path) + } + + fun read(json: JsonElement): JsonElement? { + if (json is JsonNull) return null + + return tokens.read(json) + } + + companion object { + /** + * Parse JSON string and return successful [JsonNode] or null otherwise + * + * @param jsonString JSON string to parse + * @return instance of parsed jackson [JsonNode] object, or null + */ + @JvmStatic + fun parse(jsonString: String?): JsonElement? = + jsonString?.let { + runCatching { + Json + .parseToJsonElement(it) + .takeIf { element -> + element.isNotNullOrMissing() + } + }.getOrNull() + } + } +} + +fun JsonElement.read(path: String): JsonElement? { + if (this is JsonNull) return JsonNull + + val trimmedPath = path.trim() + + val tokens = PathCompiler.compile(trimmedPath) + + return tokens.read(this) +} + +inline fun JsonElement.read(path: String, serializer: KSerializer): T? = + try { + read(path)?.let { Json.decodeFromJsonElement(serializer, it) } + } catch (_: Throwable) { + null + } + +fun JsonElement.readString(path: String): String? = + when (val value = read(path)) { + is JsonPrimitive -> value.contentOrNull + else -> null + } + +private fun List.read(json: JsonElement): JsonElement? = + fold(initial = JsonNode(json, isNewRoot = false)) { valueAtPath: JsonNode?, nextToken: Token -> + valueAtPath?.let(nextToken::read) + }?.element diff --git a/src/main/kotlin/com/nfeld/jsonpathkt/PathCompiler.kt b/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/PathCompiler.kt similarity index 79% rename from src/main/kotlin/com/nfeld/jsonpathkt/PathCompiler.kt rename to jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/PathCompiler.kt index fd74380..fb0746d 100644 --- a/src/main/kotlin/com/nfeld/jsonpathkt/PathCompiler.kt +++ b/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/PathCompiler.kt @@ -1,6 +1,16 @@ package com.nfeld.jsonpathkt -import java.lang.IllegalStateException +import com.nfeld.jsonpathkt.tokens.ArrayAccessorToken +import com.nfeld.jsonpathkt.tokens.ArrayLengthBasedRangeAccessorToken +import com.nfeld.jsonpathkt.tokens.DeepScanArrayAccessorToken +import com.nfeld.jsonpathkt.tokens.DeepScanLengthBasedArrayAccessorToken +import com.nfeld.jsonpathkt.tokens.DeepScanObjectAccessorToken +import com.nfeld.jsonpathkt.tokens.DeepScanWildcardToken +import com.nfeld.jsonpathkt.tokens.MultiArrayAccessorToken +import com.nfeld.jsonpathkt.tokens.MultiObjectAccessorToken +import com.nfeld.jsonpathkt.tokens.ObjectAccessorToken +import com.nfeld.jsonpathkt.tokens.Token +import com.nfeld.jsonpathkt.tokens.WildcardToken internal object PathCompiler { @@ -8,10 +18,10 @@ internal object PathCompiler { * @param path Path string to compile * @return List of [Token] to read against a JSON */ - @Throws(IllegalArgumentException::class) - internal fun compile(path: String): List { - if (path.isBlank()) { - throw IllegalArgumentException("Path cannot be empty") + @Throws(IllegalArgumentException::class, IllegalStateException::class) + fun compile(path: String): List { + require(path.isNotBlank()) { + "Path cannot be empty" } val tokens = mutableListOf() @@ -45,6 +55,7 @@ internal object PathCompiler { c == '*' && isDeepScan -> { isWildcard = true } + c == '.' -> { if (keyBuilder.isNotEmpty() || isWildcard) { addObjectAccessorToken() @@ -56,41 +67,52 @@ internal object PathCompiler { isDeepScan = true ++i } + '*' -> { isWildcard = true ++i } + null -> throw IllegalArgumentException("Unexpected ending with dot") } } + c == '[' -> { if (keyBuilder.isNotEmpty() || isWildcard) { addObjectAccessorToken() resetForNextToken() } val closingBracketIndex = findMatchingClosingBracket(path, i) - if (closingBracketIndex > i + 1) { // i+1 checks to make sure atleast one char in the brackets - val token = compileBracket(path, i, closingBracketIndex) - if (isDeepScan) { - val deepScanToken: Token? = when (token) { - is WildcardToken -> DeepScanWildcardToken() - is ObjectAccessorToken -> DeepScanObjectAccessorToken(listOf(token.key)) - is MultiObjectAccessorToken -> DeepScanObjectAccessorToken(token.keys) - is ArrayAccessorToken -> DeepScanArrayAccessorToken(listOf(token.index)) - is MultiArrayAccessorToken -> DeepScanArrayAccessorToken(token.indices) - is ArrayLengthBasedRangeAccessorToken -> DeepScanLengthBasedArrayAccessorToken(token.startIndex, token.endIndex, token.offsetFromEnd) - else -> null - } - deepScanToken?.let { tokens.add(it) } - resetForNextToken() - } else { - tokens.add(token) + + // i+1 checks to make sure atleast one char in the brackets + require(closingBracketIndex > i + 1) { + "Expecting closing array bracket with a value inside" + } + + val token = compileBracket(path, i, closingBracketIndex) + if (isDeepScan) { + val deepScanToken: Token? = when (token) { + is WildcardToken -> DeepScanWildcardToken() + is ObjectAccessorToken -> DeepScanObjectAccessorToken(listOf(token.key)) + is MultiObjectAccessorToken -> DeepScanObjectAccessorToken(token.keys) + is ArrayAccessorToken -> DeepScanArrayAccessorToken(listOf(token.index)) + is MultiArrayAccessorToken -> DeepScanArrayAccessorToken(token.indices) + is ArrayLengthBasedRangeAccessorToken -> DeepScanLengthBasedArrayAccessorToken( + token.startIndex, + token.endIndex, + token.offsetFromEnd + ) + + else -> null } - i = closingBracketIndex + deepScanToken?.let { tokens.add(it) } + resetForNextToken() } else { - throw IllegalArgumentException("Expecting closing array bracket with a value inside") + tokens.add(token) } + i = closingBracketIndex } + else -> keyBuilder.append(c) } ++i @@ -108,7 +130,7 @@ internal object PathCompiler { * @param openingIndex opening bracket index we are to search matching closing bracket for * @return closing bracket index, or -1 if not found */ - internal fun findMatchingClosingBracket(path: String, openingIndex: Int): Int { + fun findMatchingClosingBracket(path: String, openingIndex: Int): Int { var isQuoteOpened = false var isSingleQuote = false // either single quote or double quote opened if isQuoteOpened var i = openingIndex + 1 @@ -124,21 +146,26 @@ internal object PathCompiler { isQuoteOpened = true isSingleQuote = c == '\'' } + isSingleQuote && c == '\'' -> { isQuoteOpened = false } + !isSingleQuote && c == '"' -> { isQuoteOpened = false } } } + c == ']' && !isQuoteOpened -> return i c == '\\' && isQuoteOpened -> { if (next == '\'' || next == '\\' || next == '"') { ++i // skip this char so we don't process escaped quote - } else if (next == null) { - throw IllegalArgumentException("Unexpected char at end of path") + } else { + requireNotNull(next) { + "Unexpected char at end of path" + } } } } @@ -156,7 +183,7 @@ internal object PathCompiler { * @param closingIndex index of closing bracket * @return Compiled [Token] */ - internal fun compileBracket(path: String, openingIndex: Int, closingIndex: Int): Token { + fun compileBracket(path: String, openingIndex: Int, closingIndex: Int): Token { // isObjectAccessor is separate from expectingClosingQuote because the second you open a quote, it's always an object, // but we we can have multiple keys and thus multiple quotes opened for that object. var isObjectAccessor = false // once this is set, it cant be anything else @@ -182,16 +209,18 @@ internal object PathCompiler { keys.add(key) keyBuilder.clear() } + fun getNextCharIgnoringWhitespace(): Char { - for (n in (i+1)..closingIndex) { + for (n in i + 1..closingIndex) { val c = path[n] if (c == ' ' && !isQuoteOpened) { continue } return c } - throw IllegalStateException("") + error("Shouldn't reach this point") } + fun isBracketNext() = getNextCharIgnoringWhitespace() == ']' fun isBracketBefore() = lastChar == '[' @@ -233,7 +262,7 @@ internal object PathCompiler { } c == '\\' && isQuoteOpened -> { - when (val nextChar = path[i+1]) { + when (val nextChar = path[i + 1]) { '\\', '\'', '"' -> { keyBuilder.append(nextChar) ++i @@ -261,7 +290,7 @@ internal object PathCompiler { isWildcard = true } - (c.isDigit() && !isQuoteOpened) || (isObjectAccessor && isQuoteOpened) -> keyBuilder.append(c) + c.isDigit() && !isQuoteOpened || isObjectAccessor && isQuoteOpened -> keyBuilder.append(c) else -> throw IllegalArgumentException("Unexpected char, char=$c, index=$i") } @@ -298,10 +327,12 @@ internal object PathCompiler { MultiArrayAccessorToken(IntRange(start, end - 1).toList()) } } + hasStartColon && hasEndColon -> { // take entire list from beginning to end ArrayLengthBasedRangeAccessorToken(0, null, 0) } + hasStartColon -> { val end = keys[0].toInt(10) // exclusive if (end < 0) { @@ -312,10 +343,12 @@ internal object PathCompiler { MultiArrayAccessorToken(IntRange(0, end - 1).toList()) } } + hasEndColon -> { val start = keys[0].toInt(10) ArrayLengthBasedRangeAccessorToken(start) } + keys.size == 1 -> ArrayAccessorToken(keys[0].toInt(10)) keys.size > 1 -> MultiArrayAccessorToken(keys.map { it.toInt(10) }) else -> null @@ -328,4 +361,4 @@ internal object PathCompiler { throw IllegalArgumentException("Not a valid path") } -} \ No newline at end of file +} diff --git a/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/extension/JsonArray.kt b/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/extension/JsonArray.kt new file mode 100644 index 0000000..e8e6618 --- /dev/null +++ b/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/extension/JsonArray.kt @@ -0,0 +1,13 @@ +@file:Suppress("NOTHING_TO_INLINE") + +package com.nfeld.jsonpathkt.extension + +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement + +internal inline fun JsonArray.children(): List = map { it } + +internal inline fun JsonArray.getValueIfNotNullOrMissing(index: Int): JsonElement? { + val value = getOrNull(index) + return if (value.isNotNullOrMissing()) value else null +} diff --git a/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/extension/JsonElement.kt b/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/extension/JsonElement.kt new file mode 100644 index 0000000..69ecf5a --- /dev/null +++ b/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/extension/JsonElement.kt @@ -0,0 +1,24 @@ +@file:Suppress("NOTHING_TO_INLINE") + +package com.nfeld.jsonpathkt.extension + +import com.nfeld.jsonpathkt.JsonPath +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.decodeFromJsonElement + +inline fun JsonElement.read(jsonpath: String): T? = try { + JsonPath(jsonpath).read(this)?.let { Json.decodeFromJsonElement(it) } +} catch (_: Throwable) { + null +} + +inline fun JsonElement.read(jsonpath: JsonPath): T? = try { + jsonpath.read(this)?.let { Json.decodeFromJsonElement(it) } +} catch (_: Throwable) { + null +} + +internal inline fun JsonElement?.isNotNullOrMissing() = + this != null && this !is JsonNull diff --git a/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/extension/JsonObject.kt b/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/extension/JsonObject.kt new file mode 100644 index 0000000..58c6251 --- /dev/null +++ b/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/extension/JsonObject.kt @@ -0,0 +1,11 @@ +@file:Suppress("NOTHING_TO_INLINE") + +package com.nfeld.jsonpathkt.extension + +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject + +internal inline fun JsonObject.getValueIfNotNullOrMissing(key: String): JsonElement? { + val value = get(key) + return if (value.isNotNullOrMissing()) value else null +} diff --git a/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/tokens/ArrayAccessorToken.kt b/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/tokens/ArrayAccessorToken.kt new file mode 100644 index 0000000..3579f43 --- /dev/null +++ b/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/tokens/ArrayAccessorToken.kt @@ -0,0 +1,79 @@ +package com.nfeld.jsonpathkt.tokens + +import com.nfeld.jsonpathkt.JsonNode +import com.nfeld.jsonpathkt.extension.getValueIfNotNullOrMissing +import com.nfeld.jsonpathkt.extension.isNotNullOrMissing +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonArray + +/** + * Accesses value at [index] from JsonArray + * + * @param index index to access, can be negative which means to access from end + */ +internal data class ArrayAccessorToken(val index: Int) : Token { + override fun read(json: JsonNode): JsonNode? = read(json, index) + + companion object { + fun read(json: JsonNode, index: Int): JsonNode? = when (val element = json.element) { + is JsonArray -> when { + json.isNewRoot -> JsonNode( + element = buildJsonArray { + element.forEach { node -> + read(JsonNode(node, isNewRoot = false), index)?.element?.let { element -> + if (element.isNotNullOrMissing()) { + add(element) + } + } + } + }, + isNewRoot = true + ) + + else -> readValueAtIndex(element, index)?.let { JsonNode(it, isNewRoot = false) } + } + + is JsonNull -> null + + is JsonPrimitive -> { + when { + element.isString -> { + val str = element.content + if (index < 0) { + val indexFromLast = str.length + index + if (indexFromLast >= 0 && indexFromLast < str.length) { + JsonNode( + element = JsonPrimitive(str[indexFromLast].toString()), + isNewRoot = false + ) + } else { + null + } + } else if (index < str.length) { + JsonNode(element = JsonPrimitive(str[index].toString()), isNewRoot = false) + } else { + null + } + } + + else -> null + } + } + + else -> null + } + + private fun readValueAtIndex(arrayNode: JsonArray, index: Int): JsonElement? { + if (index < 0) { + val indexFromLast = arrayNode.size + index + if (indexFromLast >= 0) { + return arrayNode.getValueIfNotNullOrMissing(indexFromLast) + } + } + return arrayNode.getValueIfNotNullOrMissing(index) + } + } +} diff --git a/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/tokens/ArrayLengthBasedRangeAccessorToken.kt b/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/tokens/ArrayLengthBasedRangeAccessorToken.kt new file mode 100644 index 0000000..74787be --- /dev/null +++ b/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/tokens/ArrayLengthBasedRangeAccessorToken.kt @@ -0,0 +1,72 @@ +package com.nfeld.jsonpathkt.tokens + +import com.nfeld.jsonpathkt.JsonNode +import com.nfeld.jsonpathkt.extension.isNotNullOrMissing +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.buildJsonArray + +/** + * Accesses values from JsonArray in range from [startIndex] to either [endIndex] or [offsetFromEnd] from end. + * When read, value returned will be JsonArray of values at requested indices in order of values in range. + * + * @param startIndex starting index of range, inclusive. Can be negative. + * @param endIndex ending index of range, exclusive. Null if using [offsetFromEnd]. Can be positive only + * @param offsetFromEnd offset of values from end of array. 0 if using [endIndex]. Can be negative only + */ +internal data class ArrayLengthBasedRangeAccessorToken( + val startIndex: Int, + val endIndex: Int? = null, + val offsetFromEnd: Int = 0 +) : Token { + override fun read(json: JsonNode): JsonNode { + val token = when (val element = json.element) { + is JsonArray -> when { + json.isNewRoot -> { + return JsonNode( + element = buildJsonArray { + element.forEach { node -> + val nextNode = read(JsonNode(node, isNewRoot = false)) + when (val nextNodeElement = nextNode.element) { + is JsonArray -> nextNodeElement.forEach(::add) + else -> if (nextNodeElement.isNotNullOrMissing()) add(nextNodeElement) + } + } + }, + isNewRoot = true + ) + } + + else -> toMultiArrayAccessorToken(element) + } + + else -> null + } + return token?.read(json.copy(isNewRoot = false)) ?: JsonNode(element = JsonArray(emptyList()), isNewRoot = true) + } + + /** + * We know the size of the array during runtime so we can recreate the MultiArrayAccessorToken to read the values + */ + fun toMultiArrayAccessorToken(json: JsonArray): MultiArrayAccessorToken? { + val size = json.size + val start = if (startIndex < 0) { + val start = size + startIndex + if (start < 0) 0 else start // even if we're out of bounds at start, always start from first item + } else { + startIndex + } + + // use endIndex if we have it, otherwise calculate from json array length + val endInclusive = if (endIndex != null) { + endIndex - 1 + } else { + size + offsetFromEnd - 1 + } + + return if (start in 0..endInclusive) { + MultiArrayAccessorToken(IntRange(start, endInclusive).toList()) + } else { + null + } + } +} diff --git a/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/tokens/DeepScanArrayAccessorToken.kt b/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/tokens/DeepScanArrayAccessorToken.kt new file mode 100644 index 0000000..c094617 --- /dev/null +++ b/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/tokens/DeepScanArrayAccessorToken.kt @@ -0,0 +1,67 @@ +package com.nfeld.jsonpathkt.tokens + +import com.nfeld.jsonpathkt.JsonNode +import com.nfeld.jsonpathkt.extension.isNotNullOrMissing +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonArrayBuilder +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonArray + +/** + * Recursive scan for values/objects/arrays found for all [indices] specified. Returns a [JsonArray] containing results found. + * + * @param indices indices to retrieve values/objects for + */ +internal data class DeepScanArrayAccessorToken(val indices: List) : Token { + private fun scan(node: JsonNode, result: JsonArrayBuilder) { + when (val element = node.element) { + is JsonObject -> { + // traverse all key/value pairs and recursively scan underlying objects/arrays + element.values.forEach { + if (it.isNotNullOrMissing()) { + scan(JsonNode(it, isNewRoot = false), result) + } + } + } + + is JsonArray -> when { + node.isNewRoot -> { + // no need to add anything on root level, scan down next level + element.forEach { + if (it.isNotNullOrMissing()) { + scan(JsonNode(it, isNewRoot = false), result) + } + } + } + + else -> { + // first add all requested indices to our results + indices.forEach { index -> + ArrayAccessorToken(index).read(node)?.element?.let { + if (it.isNotNullOrMissing()) { + result.add(it) + } + } + } + + // now recursively scan underlying objects/arrays + element.forEach { + if (it.isNotNullOrMissing()) { + scan(JsonNode(it, isNewRoot = false), result) + } + } + } + } + + else -> {} + } + } + + override fun read(json: JsonNode): JsonNode = + JsonNode( + element = buildJsonArray { + scan(json, this) + }, + isNewRoot = true + ) +} diff --git a/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/tokens/DeepScanLengthBasedArrayAccessorToken.kt b/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/tokens/DeepScanLengthBasedArrayAccessorToken.kt new file mode 100644 index 0000000..ecc37cc --- /dev/null +++ b/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/tokens/DeepScanLengthBasedArrayAccessorToken.kt @@ -0,0 +1,71 @@ +package com.nfeld.jsonpathkt.tokens + +import com.nfeld.jsonpathkt.JsonNode +import com.nfeld.jsonpathkt.extension.isNotNullOrMissing +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonArrayBuilder +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonArray + +/** + * Recursive scan for values/objects/arrays from [JsonArray] in range from [startIndex] to either [endIndex] or [offsetFromEnd] from end. + * When read, value returned will be JsonArray of values at requested indices in order of values in range. Returns a JsonArray containing results found. + * + * @param startIndex starting index of range, inclusive. Can be negative. + * @param endIndex ending index of range, exclusive. Null if using [offsetFromEnd] + * @param offsetFromEnd offset of values from end of array. 0 if using [endIndex] + */ +internal data class DeepScanLengthBasedArrayAccessorToken( + val startIndex: Int, + val endIndex: Int? = null, + val offsetFromEnd: Int = 0 +) : Token { + private fun scan(node: JsonNode, result: JsonArrayBuilder) { + when (val element = node.element) { + is JsonObject -> { + // traverse all key/value pairs and recursively scan underlying objects/arrays + element.values.forEach { + if (it.isNotNullOrMissing()) { + scan(JsonNode(it, isNewRoot = false), result) + } + } + } + + is JsonArray -> when { + node.isNewRoot -> { + // no need to add anything on root level, scan down next level + element.forEach { + if (it.isNotNullOrMissing()) { + scan(JsonNode(it, isNewRoot = false), result) + } + } + } + + else -> { + ArrayLengthBasedRangeAccessorToken(startIndex, endIndex, offsetFromEnd) + .read(node).element.let { resultNode -> + val resultArray = resultNode as? JsonArray + resultArray?.forEach { result.add(it) } + } + + // now recursively scan underlying objects/arrays + element.forEach { + if (it.isNotNullOrMissing()) { + scan(JsonNode(it, isNewRoot = false), result) + } + } + } + } + + else -> {} + } + } + + override fun read(json: JsonNode): JsonNode = + JsonNode( + element = buildJsonArray { + scan(json, this) + }, + isNewRoot = true + ) +} diff --git a/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/tokens/DeepScanObjectAccessorToken.kt b/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/tokens/DeepScanObjectAccessorToken.kt new file mode 100644 index 0000000..38a4ce1 --- /dev/null +++ b/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/tokens/DeepScanObjectAccessorToken.kt @@ -0,0 +1,55 @@ +package com.nfeld.jsonpathkt.tokens + +import com.nfeld.jsonpathkt.JsonNode +import com.nfeld.jsonpathkt.extension.isNotNullOrMissing +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonArrayBuilder +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonArray + +/** + * Recursive scan for values with keys in [targetKeys] list. Returns a [JsonArray] containing values found. + * + * @param targetKeys keys to find values for + */ +internal data class DeepScanObjectAccessorToken(val targetKeys: List) : Token { + private fun scan(node: JsonNode, result: JsonArrayBuilder) { + when (val element = node.element) { + is JsonObject -> { + // first add all values from keys requested to our result + targetKeys.forEach { key -> + ObjectAccessorToken.read(node, key)?.element?.let { + if (it.isNotNullOrMissing()) { + result.add(it) + } + } + } + + // recursively scan all underlying objects/arrays + element.values.forEach { + if (it.isNotNullOrMissing()) { + scan(JsonNode(it, isNewRoot = false), result) + } + } + } + + is JsonArray -> { + element.forEach { + if (it.isNotNullOrMissing()) { + scan(JsonNode(it, isNewRoot = false), result) + } + } + } + + else -> {} + } + } + + override fun read(json: JsonNode): JsonNode = + JsonNode( + element = buildJsonArray { + scan(json, this) + }, + isNewRoot = true + ) +} diff --git a/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/tokens/DeepScanWildcardToken.kt b/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/tokens/DeepScanWildcardToken.kt new file mode 100644 index 0000000..8bd5f0e --- /dev/null +++ b/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/tokens/DeepScanWildcardToken.kt @@ -0,0 +1,66 @@ +package com.nfeld.jsonpathkt.tokens + +import com.nfeld.jsonpathkt.JsonNode +import com.nfeld.jsonpathkt.extension.isNotNullOrMissing +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonArrayBuilder +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonArray + +internal class DeepScanWildcardToken : Token { + private fun scan(node: JsonNode, result: JsonArrayBuilder) { + when { + node.isNewRoot -> { + // no need to add anything on root level, scan down next level + (node.element as JsonArray).forEach { + if (it.isNotNullOrMissing()) { + scan(JsonNode(it, isNewRoot = false), result) + } + } + } + + node.element is JsonObject || node.element is JsonArray -> { + WildcardToken().read(node).let { nextNode -> + if (nextNode.element is JsonArray) { + nextNode.element.forEach { + if (it.isNotNullOrMissing()) { + result.add(it) + } + } + } + } + + // now recursively scan underlying objects/arrays + when (node.element) { + is JsonArray -> node.element.forEach { + if (it.isNotNullOrMissing()) { + scan(JsonNode(it, isNewRoot = false), result) + } + } + + is JsonObject -> node.element.values.forEach { + if (it.isNotNullOrMissing()) { + scan(JsonNode(it, isNewRoot = false), result) + } + } + + else -> {} + } + } + + else -> {} + } + } + + override fun read(json: JsonNode): JsonNode = + JsonNode( + element = buildJsonArray { + scan(json, this) + }, + isNewRoot = true + ) + + override fun toString(): String = "DeepScanWildcardToken" + override fun hashCode(): Int = toString().hashCode() + override fun equals(other: Any?): Boolean = other is DeepScanWildcardToken +} diff --git a/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/tokens/MultiArrayAccessorToken.kt b/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/tokens/MultiArrayAccessorToken.kt new file mode 100644 index 0000000..f1350b1 --- /dev/null +++ b/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/tokens/MultiArrayAccessorToken.kt @@ -0,0 +1,44 @@ +package com.nfeld.jsonpathkt.tokens + +import com.nfeld.jsonpathkt.JsonNode +import com.nfeld.jsonpathkt.extension.isNotNullOrMissing +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.buildJsonArray + +/** + * Accesses values at [indices] from JsonArray. When read, value returned will be JsonArray of values + * at requested indices in given order. + * + * @param indices indices to access, can be negative which means to access from end + */ +internal data class MultiArrayAccessorToken(val indices: List) : Token { + override fun read(json: JsonNode): JsonNode { + val result = when { + json.isNewRoot -> buildJsonArray { + (json.element as JsonArray).forEach { node -> + indices.forEach { index -> + ArrayAccessorToken.read( + JsonNode(node, isNewRoot = false), + index + )?.element?.let { element -> + if (element.isNotNullOrMissing()) { + add(element) + } + } + } + } + } + + else -> buildJsonArray { + indices.forEach { index -> + ArrayAccessorToken.read(json, index)?.element?.let { element -> + if (element.isNotNullOrMissing()) { + add(element) + } + } + } + } + } + return JsonNode(result, isNewRoot = true) + } +} diff --git a/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/tokens/MultiObjectAccessorToken.kt b/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/tokens/MultiObjectAccessorToken.kt new file mode 100644 index 0000000..3240515 --- /dev/null +++ b/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/tokens/MultiObjectAccessorToken.kt @@ -0,0 +1,49 @@ +package com.nfeld.jsonpathkt.tokens + +import com.nfeld.jsonpathkt.JsonNode +import com.nfeld.jsonpathkt.extension.getValueIfNotNullOrMissing +import com.nfeld.jsonpathkt.extension.isNotNullOrMissing +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonArray + +/** + * Accesses values at [keys] from [JsonObject]. When read, value returned will be [JsonObject] + * containing key/value pairs requested. Keys that are null or don't exist won't be added in Object + * + * @param keys keys to access for which key/values to return + */ +internal data class MultiObjectAccessorToken(val keys: List) : Token { + override fun read(json: JsonNode): JsonNode = when { + json.element is JsonObject -> { + // Going from an object to a list always creates a root level list + JsonNode( + element = buildJsonArray { + keys.forEach { + json.element.getValueIfNotNullOrMissing(it)?.let(::add) + } + }, + isNewRoot = true + ) + } + + json.element is JsonArray && json.isNewRoot -> { + JsonNode( + element = buildJsonArray { + json.element.forEach { node -> + keys.forEach { key -> + ObjectAccessorToken.read(JsonNode(node, isNewRoot = false), key)?.element?.let { + if (it.isNotNullOrMissing()) { + add(it) + } + } + } + } + }, + isNewRoot = true + ) + } + + else -> JsonNode(JsonArray(emptyList()), isNewRoot = true) + } +} diff --git a/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/tokens/ObjectAccessorToken.kt b/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/tokens/ObjectAccessorToken.kt new file mode 100644 index 0000000..6633dc5 --- /dev/null +++ b/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/tokens/ObjectAccessorToken.kt @@ -0,0 +1,43 @@ +package com.nfeld.jsonpathkt.tokens + +import com.nfeld.jsonpathkt.JsonNode +import com.nfeld.jsonpathkt.extension.getValueIfNotNullOrMissing +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonArray + +/** + * Accesses value at [key] from [JsonObject] + * + * @param key key to access + */ +internal data class ObjectAccessorToken(val key: String) : Token { + override fun read(json: JsonNode): JsonNode? = read(json, key) + + companion object { + fun read(json: JsonNode, key: String): JsonNode? = when { + json.element is JsonObject -> json.element.getValueIfNotNullOrMissing(key)?.let { + JsonNode(it, isNewRoot = false) + } + + json.element is JsonArray && json.isNewRoot -> { + // we're at root level and can get children from objects + JsonNode( + element = buildJsonArray { + json.element.forEach { node -> + if (node is JsonObject) { + node.getValueIfNotNullOrMissing(key)?.let { + add(it) + } + } + } + }, + isNewRoot = true + ) + } + // JsonArray should return null, unless it's the RootLevelArrayNode. This is intentional + // everything else is scalar and not accessible + else -> null + } + } +} diff --git a/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/tokens/Token.kt b/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/tokens/Token.kt new file mode 100644 index 0000000..673a15d --- /dev/null +++ b/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/tokens/Token.kt @@ -0,0 +1,10 @@ +package com.nfeld.jsonpathkt.tokens + +import com.nfeld.jsonpathkt.JsonNode + +interface Token { + /** + * Takes in JsonElement and outputs next JsonElement or value by evaluating token against current object/array in path + */ + fun read(json: JsonNode): JsonNode? +} diff --git a/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/tokens/WildcardToken.kt b/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/tokens/WildcardToken.kt new file mode 100644 index 0000000..e6fe958 --- /dev/null +++ b/jsonpath/src/commonMain/kotlin/com/nfeld/jsonpathkt/tokens/WildcardToken.kt @@ -0,0 +1,69 @@ +package com.nfeld.jsonpathkt.tokens + +import com.nfeld.jsonpathkt.JsonNode +import com.nfeld.jsonpathkt.extension.isNotNullOrMissing +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonArray + +/** + * Returns all values from an Object, or the same list + */ +internal class WildcardToken : Token { + override fun read(json: JsonNode): JsonNode = when (val element = json.element) { + is JsonObject -> { + JsonNode( + element = buildJsonArray { + element.values.forEach { + if (it.isNotNullOrMissing()) { + add(it) + } + } + }, + isNewRoot = true + ) + } + + is JsonArray -> { + if (!json.isNewRoot) { + // copy over children into our special JsonArray to hold underlying items + json.copy(isNewRoot = true) + } else { + JsonNode( + element = buildJsonArray { + // iterate through each item and move everything up one level + element.forEach { element -> + when (element) { + is JsonObject -> { + element.values.forEach { + if (it.isNotNullOrMissing()) { + add(it) + } + } + } + + is JsonArray -> { + // move all items from this node to result node + element.forEach { + if (it.isNotNullOrMissing()) { + add(it) + } + } + } + // anything else gets dropped since it's on rootmost level + else -> {} + } + } + }, + isNewRoot = true + ) + } + } + + else -> json.copy(isNewRoot = false) + } + + override fun toString(): String = "WildcardToken" + override fun hashCode(): Int = toString().hashCode() + override fun equals(other: Any?): Boolean = other is WildcardToken +} diff --git a/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/JsonNodeTest.kt b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/JsonNodeTest.kt new file mode 100644 index 0000000..ff3763d --- /dev/null +++ b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/JsonNodeTest.kt @@ -0,0 +1,21 @@ +package com.nfeld.jsonpathkt + +import com.nfeld.jsonpathkt.extension.read +import kotlin.test.Test +import kotlin.test.assertEquals + +class JsonNodeTest { + @Test + fun should_read_root_JsonArray() { + val jsonObj = readTree(SMALL_JSON_ARRAY) + assertEquals(2, jsonObj.read(JsonPath("$[1]"))!!) + assertEquals(2, jsonObj.read("$[1]")!!) + } + + @Test + fun should_read_root_JsonObject() { + val jsonObj = readTree(SMALL_JSON) + assertEquals(5, jsonObj.read(JsonPath("$['key']"))!!) + assertEquals(5, jsonObj.read("$['key']")!!) + } +} diff --git a/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/PathCompilerTest.kt b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/PathCompilerTest.kt new file mode 100644 index 0000000..79a65bc --- /dev/null +++ b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/PathCompilerTest.kt @@ -0,0 +1,285 @@ +package com.nfeld.jsonpathkt + +import com.nfeld.jsonpathkt.tokens.ArrayAccessorToken +import com.nfeld.jsonpathkt.tokens.ArrayLengthBasedRangeAccessorToken +import com.nfeld.jsonpathkt.tokens.DeepScanArrayAccessorToken +import com.nfeld.jsonpathkt.tokens.DeepScanLengthBasedArrayAccessorToken +import com.nfeld.jsonpathkt.tokens.DeepScanObjectAccessorToken +import com.nfeld.jsonpathkt.tokens.DeepScanWildcardToken +import com.nfeld.jsonpathkt.tokens.MultiArrayAccessorToken +import com.nfeld.jsonpathkt.tokens.MultiObjectAccessorToken +import com.nfeld.jsonpathkt.tokens.ObjectAccessorToken +import com.nfeld.jsonpathkt.tokens.WildcardToken +import io.kotest.matchers.shouldBe +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class PathCompilerTest { + @Test + fun compile() { + val f = PathCompiler::compile + + assertEquals( + listOf( + ArrayAccessorToken(2), + DeepScanObjectAccessorToken(listOf("name", "id")) + ), f("$[2]..['name','id']") + ) + assertEquals( + listOf( + ArrayAccessorToken(2), + DeepScanObjectAccessorToken(listOf("name", "id")), + ArrayAccessorToken(2) + ), f("$[2]..['name','id'][2]") + ) + + assertEquals(listOf(DeepScanObjectAccessorToken(listOf("name"))), f("$..['name']")) + assertEquals( + listOf(DeepScanObjectAccessorToken(listOf("name", "age"))), + f("$..['name','age']") + ) + assertEquals(listOf(DeepScanArrayAccessorToken(listOf(0))), f("$..[0]")) + assertEquals(listOf(DeepScanArrayAccessorToken(listOf(0, 1, 6))), f("$..[0,1,6]")) + assertEquals(listOf(DeepScanArrayAccessorToken(listOf(0, -1, -6))), f("$..[0,-1,-6]")) + assertEquals(listOf(DeepScanArrayAccessorToken(listOf(-2))), f("$..[-2]")) + assertEquals(listOf(DeepScanArrayAccessorToken(listOf(0, 1, 2))), f("$..[0:3]")) + assertEquals(listOf(DeepScanArrayAccessorToken(listOf(0, 1, 2))), f("$..[:3]")) + assertEquals(listOf(DeepScanLengthBasedArrayAccessorToken(1, null, 0)), f("$..[1:]")) + assertEquals(listOf(DeepScanLengthBasedArrayAccessorToken(0, null, -2)), f("$..[:-2]")) + assertEquals(listOf(DeepScanLengthBasedArrayAccessorToken(-5, null, 0)), f("$..[-5:]")) + assertEquals(listOf(DeepScanLengthBasedArrayAccessorToken(0, null, 0)), f("$..[:]")) + assertEquals(listOf(DeepScanLengthBasedArrayAccessorToken(0, null, -2)), f("$..[0:-2]")) + assertEquals(listOf(DeepScanLengthBasedArrayAccessorToken(-5, 6, 0)), f("$..[-5:6]")) + assertEquals(listOf(DeepScanLengthBasedArrayAccessorToken(-5, null, -2)), f("$..[-5:-2]")) + assertEquals(listOf(ObjectAccessorToken("-")), f("$-")) + assertEquals(listOf(ObjectAccessorToken("-0")), f("$-0")) + assertEquals(listOf(ObjectAccessorToken("-"), ArrayAccessorToken(0)), f("$-[0]")) + assertEquals(listOf(WildcardToken()), f("$.*")) + assertEquals(listOf(WildcardToken(), ObjectAccessorToken("key")), f("$.*.key")) + assertEquals(listOf(WildcardToken(), ArrayAccessorToken(3)), f("$.*[3]")) + assertEquals( + listOf(WildcardToken(), DeepScanObjectAccessorToken(listOf("key"))), + f("$.*..key") + ) + assertEquals( + listOf(WildcardToken(), DeepScanArrayAccessorToken(listOf(1, 2, 3))), + f("$.*..[1:4]") + ) + f("""$..["key"]""") shouldBe listOf(DeepScanObjectAccessorToken(listOf("key"))) + f("$..*") shouldBe listOf(DeepScanWildcardToken()) + f("$..[*]") shouldBe listOf(DeepScanWildcardToken()) + f("$..*..*") shouldBe listOf(DeepScanWildcardToken(), DeepScanWildcardToken()) + f("$..[*]..[*]") shouldBe listOf(DeepScanWildcardToken(), DeepScanWildcardToken()) + } + + @Test + fun should_compile_without_root_token() { + val f = PathCompiler::compile + + assertEquals(listOf(ObjectAccessorToken("key")), f("key")) + assertEquals(listOf(ObjectAccessorToken("key")), f("['key']")) + assertEquals(listOf(ObjectAccessorToken("key")), f("""["key"]""")) + assertEquals(listOf(ObjectAccessorToken("*")), f("*")) + assertEquals(listOf(ObjectAccessorToken("key"), ArrayAccessorToken(4)), f("key[4]")) + assertEquals(listOf(MultiObjectAccessorToken(listOf("a", "b"))), f("['a','b']")) + assertEquals(listOf(MultiObjectAccessorToken(listOf("a", "b"))), f("""["a","b"]""")) + assertEquals(listOf(ArrayAccessorToken(3)), f("[3]")) + assertEquals(listOf(MultiArrayAccessorToken(listOf(3, 4))), f("[3,4]")) + assertEquals(listOf(MultiArrayAccessorToken(listOf(0, 1, 2))), f("[:3]")) + assertEquals(listOf(MultiArrayAccessorToken(listOf(0, 1, 2))), f("[0:3]")) + assertEquals(listOf(ArrayLengthBasedRangeAccessorToken(1, null, 0)), f("[1:]")) + } + + @Test + fun should_find_matching_closing_bracket() { + val start = 0 + val f = PathCompiler::findMatchingClosingBracket + + assertEquals(1, f("[]", start)) + assertEquals(2, f("[5]", start)) + assertEquals(3, f("[53]", start)) + assertEquals(4, f("['5']", start)) + assertEquals(3, f("[-5]", start)) + assertEquals(4, f("[-5:]", start)) + assertEquals(3, f("[:5]", start)) + assertEquals(4, f("[0:5]", start)) + assertEquals(2, f("[:]", start)) + assertEquals(6, f("[0,1,2]", start)) + assertEquals(5, f("['a[']", start)) + assertEquals(5, f("['a]']", start)) + assertEquals(7, f("['a\\'b']", start)) + assertEquals(9, f("['a\\'\\']']", start)) + assertEquals(6, f("['4\\a']", start)) + assertEquals(7, f("""["a\"b"]""", start)) + assertEquals(9, f("""["a\"\"]"]""", start)) + assertEquals(6, f("""["4\a"]""", start)) + assertEquals(2, f("[*]", start)) + } + + @Test + fun compileBracket() { + val f = PathCompiler::compileBracket + val start = 1 + var end = 0 + + fun findClosingIndex(path: String): String { + println("Testing $path") + end = PathCompiler.findMatchingClosingBracket(path, start) + return path + } + + assertEquals(ArrayAccessorToken(0), f(findClosingIndex("$[0]"), start, end)) + assertEquals(ArrayAccessorToken(-4), f(findClosingIndex("$[-4]"), start, end)) + assertEquals( + MultiArrayAccessorToken(listOf(0, 1, 2)), + f(findClosingIndex("$[:3]"), start, end) + ) + assertEquals( + ArrayLengthBasedRangeAccessorToken(3, null, 0), + f(findClosingIndex("$[3:]"), start, end) + ) + assertEquals( + ArrayLengthBasedRangeAccessorToken(0, null, 0), + f(findClosingIndex("$[:]"), start, end) + ) + assertEquals( + MultiArrayAccessorToken(listOf(1, 2, 3)), + f(findClosingIndex("$[1:4]"), start, end) + ) + assertEquals( + MultiArrayAccessorToken(listOf(1, 2, 3)), + f(findClosingIndex("$[1,2,3]"), start, end) + ) + assertEquals( + MultiArrayAccessorToken(listOf(1, -2, 3)), + f(findClosingIndex("$[1,-2,3]"), start, end) + ) + assertEquals(ObjectAccessorToken("name"), f(findClosingIndex("$['name']"), start, end)) + assertEquals(ObjectAccessorToken("4"), f(findClosingIndex("$['4']"), start, end)) + assertEquals( + MultiObjectAccessorToken(listOf("name", "age")), + f(findClosingIndex("$['name','age']"), start, end) + ) + assertEquals( + MultiObjectAccessorToken(listOf("name", "age", "4")), + f(findClosingIndex("$['name','age',4]"), start, end) + ) + assertEquals( + ObjectAccessorToken("name:age"), + f(findClosingIndex("$['name:age']"), start, end) + ) + assertEquals(WildcardToken(), f(findClosingIndex("$[*]"), start, end)) + assertEquals( + ObjectAccessorToken(""":@."$,*'\"""), + f(findClosingIndex("""$[':@."$,*\'\\']"""), start, end) + ) + assertEquals(ObjectAccessorToken(""), f(findClosingIndex("$['']"), start, end)) + assertEquals(ObjectAccessorToken(""), f(findClosingIndex("$[\"\"]"), start, end)) + assertEquals(ObjectAccessorToken("\\"), f(findClosingIndex("$['\\\\']"), start, end)) + assertEquals(ObjectAccessorToken("'"), f(findClosingIndex("$['\\'']"), start, end)) + assertEquals(ObjectAccessorToken("'"), f(findClosingIndex("$[\"'\"]"), start, end)) + assertEquals(ObjectAccessorToken("\""), f(findClosingIndex("$['\"']"), start, end)) + assertEquals(ObjectAccessorToken("\""), f(findClosingIndex("""$["\""]"""), start, end)) + + // handle negative values in array ranges + assertEquals( + ArrayLengthBasedRangeAccessorToken(0, null, -1), + f(findClosingIndex("$[:-1]"), start, end) + ) + assertEquals( + ArrayLengthBasedRangeAccessorToken(0, null, -3), + f(findClosingIndex("$[:-3]"), start, end) + ) + assertEquals( + ArrayLengthBasedRangeAccessorToken(-1, null, 0), + f(findClosingIndex("$[-1:]"), start, end) + ) + assertEquals( + ArrayLengthBasedRangeAccessorToken(-5, null, 0), + f(findClosingIndex("$[-5:]"), start, end) + ) + assertEquals( + ArrayLengthBasedRangeAccessorToken(-5, null, -1), + f(findClosingIndex("$[-5:-1]"), start, end) + ) + assertEquals( + ArrayLengthBasedRangeAccessorToken(5, null, -1), + f(findClosingIndex("$[5:-1]"), start, end) + ) + assertEquals( + ArrayLengthBasedRangeAccessorToken(-5, 4, 0), + f(findClosingIndex("$[-5:4]"), start, end) + ) + + // ignore space paddings + assertEquals(ArrayAccessorToken(0), f(findClosingIndex("$[ 0 ]"), start, end)) + assertEquals( + MultiArrayAccessorToken(listOf(0, 3)), + f(findClosingIndex("$[0, 3]"), start, end) + ) + assertEquals( + ArrayLengthBasedRangeAccessorToken(0, null, 0), + f(findClosingIndex("$[ : ]"), start, end) + ) + assertEquals( + ArrayLengthBasedRangeAccessorToken(2, null, 0), + f(findClosingIndex("$[ 2 : ]"), start, end) + ) + assertEquals( + MultiArrayAccessorToken(listOf(0, 1)), + f(findClosingIndex("$[ : 2 ]"), start, end) + ) + assertEquals( + MultiArrayAccessorToken(listOf(1, 2)), + f(findClosingIndex("$[ 1 : 3 ]"), start, end) + ) + assertEquals(WildcardToken(), f(findClosingIndex("$[ * ]"), start, end)) + assertEquals(ObjectAccessorToken("name"), f(findClosingIndex("$[ 'name' ]"), start, end)) + + // double quotes should be identical to single quotes + f(findClosingIndex("""$["key"]"""), start, end) shouldBe ObjectAccessorToken("key") + f(findClosingIndex("""$["'key'"]"""), start, end) shouldBe ObjectAccessorToken("'key'") + f(findClosingIndex("""$["ke'y"]"""), start, end) shouldBe ObjectAccessorToken("ke'y") + f(findClosingIndex("""$["ke\"y"]"""), start, end) shouldBe ObjectAccessorToken("ke\"y") + f(findClosingIndex("""$["key","key2"]"""), start, end) shouldBe MultiObjectAccessorToken( + listOf("key", "key2") + ) + } + + @Test + fun should_throw() { + val compile = PathCompiler::compile + val compileBracket = PathCompiler::compileBracket + + assertFailsWith { compile("") } // path cannot be empty + assertFailsWith { compile("$[]") } // needs value in brackets + assertFailsWith { compile("$[") } // needs closing bracket + assertFailsWith { compile("$[[]") } // invalid char at end + assertFailsWith { compile("$[[]]") } + assertFailsWith { compile("$[[0]]") } + assertFailsWith { compile("$[0[0]]") } + assertFailsWith { + compileBracket( + "$[]", + 1, + 2 + ) + } // no token returned + assertFailsWith { compile("$.") } // needs closing bracket + assertFailsWith { compile("$['\\") } // unexpected escape char + assertFailsWith { + PathCompiler.findMatchingClosingBracket( + "$['4\\", + 1 + ) + } + assertFailsWith { compile("$[-'0']") } // cant use both negative and object accessor + assertFailsWith { compile("$-[]") } + assertFailsWith { compile("$['single'quote']") } + assertFailsWith { compile("$[*,1]") } + assertFailsWith { compile("""$["'key"']""") } + assertFailsWith { compile("""$['"key'"]""") } + assertFailsWith { compile("""['a'.'b']""") } + } +} diff --git a/src/test/kotlin/com/nfeld/jsonpathkt/TestUtil.kt b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/json_fixtures.kt similarity index 93% rename from src/test/kotlin/com/nfeld/jsonpathkt/TestUtil.kt rename to jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/json_fixtures.kt index 226bb99..19b43e2 100644 --- a/src/test/kotlin/com/nfeld/jsonpathkt/TestUtil.kt +++ b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/json_fixtures.kt @@ -1,45 +1,5 @@ package com.nfeld.jsonpathkt -import com.fasterxml.jackson.databind.JsonNode -import com.jayway.jsonpath.Configuration -import com.jayway.jsonpath.spi.json.JacksonJsonProvider -import com.nfeld.jsonpathkt.cache.CacheProvider -import com.nfeld.jsonpathkt.util.JacksonUtil - -fun readTree(json: String): JsonNode = JacksonUtil.mapper.readTree(json) - -// we need to reset this singleton across test suites -fun resetCacheProvider() { - // use reflection to reset CacheProvider singleton to its initial state - CacheProvider.javaClass.getDeclaredField("cache").apply { - isAccessible = true - set(null, null) - } - CacheProvider.javaClass.getDeclaredField("useDefault").apply { - isAccessible = true - setBoolean(null, true) - } -} - -fun resetJaywayCacheProvider() { - com.jayway.jsonpath.spi.cache.CacheProvider::class.java.getDeclaredField("cache").apply { - isAccessible = true - set(null, null) - } - com.jayway.jsonpath.spi.cache.CacheProvider::class.java.getDeclaredField("cachingEnabled").apply { - isAccessible = true - setBoolean(null, false) - } -} - -fun readFromJayway(json: String, path: String): String { - val jaywayConfig = Configuration.defaultConfiguration().jsonProvider(JacksonJsonProvider()) - val documentContext = com.jayway.jsonpath.JsonPath.parse(json, jaywayConfig) - val result = documentContext.read(path).toString() - println("Jayway result for $path: $result") - return result -} - const val SMALL_JSON = "{\"key\": 5}" const val SMALL_JSON_ARRAY = "[1,2,3,4, $SMALL_JSON]" const val LARGE_JSON = """[{ @@ -433,4 +393,4 @@ const val FAMILY_JSON = """ ] } } - """ \ No newline at end of file + """ diff --git a/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/path/ArrayAccessorsTest.kt b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/path/ArrayAccessorsTest.kt new file mode 100644 index 0000000..beb2241 --- /dev/null +++ b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/path/ArrayAccessorsTest.kt @@ -0,0 +1,54 @@ +package com.nfeld.jsonpathkt.path + +import com.nfeld.jsonpathkt.JsonPath +import com.nfeld.jsonpathkt.LARGE_JSON +import com.nfeld.jsonpathkt.SMALL_JSON_ARRAY +import com.nfeld.jsonpathkt.extension.read +import io.kotest.matchers.shouldBe +import kotlinx.serialization.json.JsonElement +import kotlin.test.Test + +class ArrayAccessorsTest { + @Test + fun parse_should_be_null_of_index_out_of_bounds() { + JsonPath.parse(SMALL_JSON_ARRAY)!!.read("$[43]") shouldBe null + JsonPath.parse(SMALL_JSON_ARRAY)!!.read("$[-43]") shouldBe null + } + + @Test + fun parse_should_get_value_if_value_exists_at_index() { + JsonPath.parse(SMALL_JSON_ARRAY)!!.read("$[2]") shouldBe 3 + JsonPath.parse(SMALL_JSON_ARRAY)!!.read("$[0]") shouldBe 1 + } + + @Test + fun parse_should_get_value_from_ends() { + JsonPath.parse(SMALL_JSON_ARRAY)!!.read("$[-2]") shouldBe 4 + JsonPath.parse(SMALL_JSON_ARRAY)!!.read("$[-4]") shouldBe 2 + JsonPath.parse(LARGE_JSON)!!.read("$[0]['tags'][-1]") shouldBe "qui" + JsonPath.parse(LARGE_JSON)!!.read("$[0]['tags'][-3]") shouldBe "cillum" + } + + @Test + fun parse_negative_0_should_get_first_item_in_array() { + JsonPath.parse(SMALL_JSON_ARRAY)!!.read("$[-0]") shouldBe 1 + } + + @Test + fun parse_should_return_null_if_used_on_JSON_object() { + JsonPath.parse("""{"key":3}""")!!.read("$[3]") shouldBe null + } + + @Test + fun parse_should_return_null_if_used_on_a_scalar_other_than_String() { + JsonPath.parse("5")!!.read("$[0]") shouldBe null + JsonPath.parse("5.34")!!.read("$[0]") shouldBe null + JsonPath.parse("true")!!.read("$[0]") shouldBe null + JsonPath.parse("false")!!.read("$[0]") shouldBe null + } + + @Test + fun parse_should_get_character_at_index_if_String_scalar() { + JsonPath.parse(""""hello"""")!!.read("$[0]") shouldBe "h" + } +} diff --git a/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/path/ArrayRangesTest.kt b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/path/ArrayRangesTest.kt new file mode 100644 index 0000000..c1d01f7 --- /dev/null +++ b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/path/ArrayRangesTest.kt @@ -0,0 +1,136 @@ +package com.nfeld.jsonpathkt.path + +import com.nfeld.jsonpathkt.FAMILY_JSON +import com.nfeld.jsonpathkt.JsonPath +import com.nfeld.jsonpathkt.LARGE_JSON +import com.nfeld.jsonpathkt.extension.read +import io.kotest.matchers.shouldBe +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonPrimitive +import kotlin.test.Test + +class ArrayRangesTest { + @Test + fun parse_should_handle_array_range_from_start() { + JsonPath.parse(LARGE_JSON)!!.read>("$[0]['tags'][:3]") shouldBe listOf( + "occaecat", + "mollit", + "ullamco" + ) + JsonPath.parse(LARGE_JSON)!!.read>("$[0]['tags'][:-4]") shouldBe listOf( + "occaecat", + "mollit", + "ullamco" + ) + } + + @Test + fun parse_should_handle_array_range_to_end() { + JsonPath.parse(LARGE_JSON)!! + .read>("$[0]['tags'][5:]") shouldBe listOf("laboris", "qui") + JsonPath.parse(LARGE_JSON)!! + .read>("$[0]['tags'][-2:]") shouldBe listOf("laboris", "qui") + } + + @Test + fun parse_should_handle_specified_range_exclusive_at_end() { + JsonPath.parse(LARGE_JSON)!! + .read>("$[0]['tags'][3:5]") shouldBe listOf("labore", "cillum") + JsonPath.parse(LARGE_JSON)!!.read>("$[0]['tags'][3:-1]") shouldBe listOf( + "labore", + "cillum", + "laboris" + ) + JsonPath.parse(LARGE_JSON)!!.read>("$[0]['tags'][-6:4]") shouldBe listOf( + "mollit", + "ullamco", + "labore" + ) + JsonPath.parse(LARGE_JSON)!!.read>("$[0]['tags'][-3:-1]") shouldBe listOf( + "cillum", + "laboris" + ) + } + + @Test + fun parse_should_return_range_items_up_to_end_if_end_index_out_of_bounds() { + JsonPath.parse(LARGE_JSON)!! + .read>("$[0]['tags'][5:30]") shouldBe listOf("laboris", "qui") + } + + @Test + fun parse_should_return_range_items_up_from_start_if_start_index_out_of_bounds() { + JsonPath.parse("""["first", "second", "third"]""")!! + .read>("$[-4:]") shouldBe listOf("first", "second", "third") + } + + @Test + fun parse_should_return_empty_list_if_used_on_JSON_object() { + JsonPath.parse("""{"key":3}""")!!.read("$[1:3]")?.toString() shouldBe "[]" + } + + @Test + fun parse_should_get_all_items_in_list() { + JsonPath.parse("""["first", "second"]""")!!.read>("$[:]") shouldBe listOf( + "first", + "second" + ) + JsonPath.parse("""["first", "second"]""")!!.read>("$[0:]") shouldBe listOf( + "first", + "second" + ) + JsonPath.parse("""["first", "second"]""")!!.read>("$") shouldBe listOf( + "first", + "second" + ) + + val expected = listOf( + mapOf( + "name" to JsonPrimitive("Thomas"), + "age" to JsonPrimitive(13) + ), + mapOf( + "name" to JsonPrimitive("Mila"), + "age" to JsonPrimitive(18) + ), + mapOf( + "name" to JsonPrimitive("Konstantin"), + "age" to JsonPrimitive(29), + "nickname" to JsonPrimitive("Kons") + ), + mapOf( + "name" to JsonPrimitive("Tracy"), + "age" to JsonPrimitive(4) + ) + ) + JsonPath.parse(FAMILY_JSON)!! + .read>>("$.family.children[:]") shouldBe expected + JsonPath.parse(FAMILY_JSON)!! + .read>>("$.family.children[0:]") shouldBe expected + } + + @Test + fun parse_entire_range_combos() { + val json = """[{"c":"cc1","d":"dd1","e":"ee1"},{"c":"cc2","d":"dd2","e":"ee2"}]""" + JsonPath.parse(json)!!.read("$[:]") + .toString() shouldBe """[{"c":"cc1","d":"dd1","e":"ee1"},{"c":"cc2","d":"dd2","e":"ee2"}]""" + JsonPath.parse(json)!!.read("$[:]['c']") + .toString() shouldBe """["cc1","cc2"]""" + JsonPath.parse(json)!!.read("$[:]['c','d']") + .toString() shouldBe """["cc1","dd1","cc2","dd2"]""" + JsonPath.parse(json)!!.read("$..[:]") + .toString() shouldBe """[{"c":"cc1","d":"dd1","e":"ee1"},{"c":"cc2","d":"dd2","e":"ee2"}]""" + JsonPath.parse(json)!!.read("$.*[:]").toString() shouldBe """[]""" + + val json2 = "[1,[2],[3,4],[5,6,7]]" + JsonPath.parse(json2)!!.read("$[:]") + .toString() shouldBe """[1,[2],[3,4],[5,6,7]]""" + JsonPath.parse(json2)!!.read("$[:][0]").toString() shouldBe """[2,3,5]""" + JsonPath.parse(json2)!!.read("$[:][1]").toString() shouldBe """[4,6]""" // !!!! + JsonPath.parse(json2)!!.read("$.*[:]").toString() shouldBe """[2,3,4,5,6,7]""" + JsonPath.parse(json2)!!.read("$..[:]") + .toString() shouldBe """[1,[2],[3,4],[5,6,7],2,3,4,5,6,7]""" + JsonPath.parse(json2)!!.read("$..[:].*") + .toString() shouldBe """[2,3,4,5,6,7]""" + } +} diff --git a/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/path/CoreParseTest.kt b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/path/CoreParseTest.kt new file mode 100644 index 0000000..6cfdfb7 --- /dev/null +++ b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/path/CoreParseTest.kt @@ -0,0 +1,101 @@ +package com.nfeld.jsonpathkt.path + +import com.nfeld.jsonpathkt.BOOKS_JSON +import com.nfeld.jsonpathkt.JsonPath +import com.nfeld.jsonpathkt.LARGE_JSON +import com.nfeld.jsonpathkt.SMALL_JSON +import com.nfeld.jsonpathkt.SMALL_JSON_ARRAY +import com.nfeld.jsonpathkt.extension.read +import com.nfeld.jsonpathkt.readTree +import io.kotest.matchers.shouldBe +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlin.test.Test + +class CoreParseTest { + @Test + fun parse_should_be_null_on_parse_failure() { + JsonPath.parse("5{}") shouldBe null + JsonPath.parse("5[]") shouldBe null + JsonPath.parse("{") shouldBe null + JsonPath.parse("") shouldBe null + JsonPath.parse("{]") shouldBe null + JsonPath.parse("[}") shouldBe null + JsonPath.parse("null") shouldBe null + JsonPath.parse(null) shouldBe null + } + + @Test + fun parse_should_be_null_if_root_node_is_Null() { + JsonPath("$").read(JsonNull) shouldBe null + } + + // What's the reason for this? +// @Test +// fun parse_should_be_null_when_parsing_root_string_but_without_quotes() { +// JsonPath.parse("hello") shouldBe null +// } + + @Test + fun parse_should_parse_root_string_with_quotes() { + JsonPath.parse(""""hello"""") shouldBe JsonPrimitive("hello") + JsonPath.parse(""""hello"""")!!.read("$") shouldBe "hello" + } + + @Test + fun parse_should_parse_root_values_other_than_String() { + JsonPath.parse("4") shouldBe JsonPrimitive(4) + JsonPath.parse("4")!!.read("$") shouldBe 4 + JsonPath.parse("4.76") shouldBe JsonPrimitive(4.76) + JsonPath.parse("4.76")!!.read("$") shouldBe 4.76 + JsonPath.parse("true") shouldBe JsonPrimitive(true) + JsonPath.parse("true")!!.read("$") shouldBe true + JsonPath.parse("false") shouldBe JsonPrimitive(false) + JsonPath.parse("false")!!.read("$") shouldBe false + } + + @Test + fun parse_should_be_able_to_get_JsonObject() { + JsonPath.parse(SMALL_JSON)!!.read("$") shouldBe readTree(SMALL_JSON) + } + + @Test + fun parse_should_be_able_to_get_JsonArray() { + JsonPath.parse(SMALL_JSON_ARRAY)!!.read("$") shouldBe readTree( + SMALL_JSON_ARRAY + ) + } + + @Test + fun parse_should_be_able_to_get_inner_JsonObjects() { + val json = """[{"outer": {"inner": 9} }]""" + JsonPath.parse(json)!!.read("$[0]") shouldBe readTree(json).jsonArray[0] + JsonPath.parse(json)!! + .read("$[0].outer") shouldBe readTree(json).jsonArray[0].jsonObject["outer"] + } + + @Test + fun parse_should_get_values_deep_in_JSON() { + JsonPath.parse(LARGE_JSON)!! + .read("$[0].friends[1].other.a.b['c']") shouldBe "yo" + JsonPath.parse(LARGE_JSON)!! + .read("$[0].friends[-1]['name']") shouldBe "Harrell Pratt" + } + + @Test + fun parse_should_preserve_order() { + JsonPath.parse(BOOKS_JSON)!!.read>("$.store..price") shouldBe listOf( + 8.95, + 12.99, + 8.99, + 22.99, + 19.95 + ) + JsonPath.parse("""{"d": 4, "f": 6, "e": 5, "a": 1, "b": 2, "c": 3}""")!! + .read>("$.*") shouldBe listOf(4, 6, 5, 1, 2, 3) + } +} diff --git a/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/path/DeepScansTest.kt b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/path/DeepScansTest.kt new file mode 100644 index 0000000..0a11068 --- /dev/null +++ b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/path/DeepScansTest.kt @@ -0,0 +1,177 @@ +package com.nfeld.jsonpathkt.path + +import com.nfeld.jsonpathkt.JsonPath +import com.nfeld.jsonpathkt.LARGE_JSON +import com.nfeld.jsonpathkt.extension.read +import io.kotest.matchers.shouldBe +import kotlinx.serialization.json.JsonElement +import kotlin.test.Test + +class DeepScansTest { + @Test + fun parse_should_get_String_list() { + val expected = listOf( + "Salazar Casey", + "Kathrine Osborn", + "Vonda Howe", + "Harrell Pratt", + "Porter Cummings", + "Mason Leach", + "Spencer Valenzuela", + "Hope Medina", + "Marie Hampton", + "Felecia Bright", + "Maryanne Wiggins", + "Marylou Caldwell", + "Mari Pugh", + "Rios Norton", + "Judy Good", + "Rosetta Stanley", + "Margret Quinn", + "Lora Cotton", + "Gaines Henry", + "Dorothea Irwin" + ) + JsonPath.parse(LARGE_JSON)!!.read>("$..name") shouldBe expected + JsonPath.parse(LARGE_JSON)!!.read>("$..name") shouldBe expected + } + + @Test + fun parse_should_get_Double_list() { + JsonPath.parse(LARGE_JSON)!!.read>("$..latitude") shouldBe listOf( + -85.888651, + 71.831798, + 78.266157, + -10.214391, + 32.293366 + ) + JsonPath.parse(LARGE_JSON)!!.read>("$..['latitude']") shouldBe listOf( + -85.888651, + 71.831798, + 78.266157, + -10.214391, + 32.293366 + ) + } + + @Test + fun parse_should_get_JsonArray() { + val expected = + """[["occaecat","mollit","ullamco","labore","cillum","laboris","qui"],["aliquip","cillum","qui","ut","ea","eu","reprehenderit"],["nulla","elit","ipsum","pariatur","ullamco","ut","sint"],["fugiat","sit","ad","voluptate","officia","aute","duis"],["est","dolor","dolore","exercitation","minim","dolor","pariatur"]]""" + JsonPath.parse(LARGE_JSON)!!.read("$..tags")?.toString() shouldBe expected + JsonPath.parse(LARGE_JSON)!!.read("$..['tags']")?.toString() shouldBe expected + } + + @Test + fun parse_should_get_from_longer_path() { + JsonPath.parse(LARGE_JSON)!!.read>("$[2]..name") shouldBe listOf( + "Marie Hampton", + "Felecia Bright", + "Maryanne Wiggins", + "Marylou Caldwell" + ) + JsonPath.parse(LARGE_JSON)!!.read>("$[2]..['name']") shouldBe listOf( + "Marie Hampton", + "Felecia Bright", + "Maryanne Wiggins", + "Marylou Caldwell" + ) + } + + @Test + fun parse_should_scan_to_get_the_first_item_of_all_sublists() { + JsonPath.parse(LARGE_JSON)!!.read("$[2]..[0]") + .toString() shouldBe """["nulla",{"id":0,"name":"Felecia Bright","other":{"a":{"b":{"c":"yo"}}}}]""" + } + + @Test + fun parse_should_scan_to_get_the_last_item_of_all_sublists() { + JsonPath.parse(LARGE_JSON)!!.read("$[2]..[-1]") + .toString() shouldBe """["sint",{"id":2,"name":"Marylou Caldwell","other":{"a":{"b":{"c":"yo"}}}}]""" + } + + @Test + fun parse_should_scan_to_get_the_first_and_third_items_of_all_sublists() { + JsonPath.parse(LARGE_JSON)!!.read("$[2]..[0,2]") + .toString() shouldBe """["nulla","ipsum",{"id":0,"name":"Felecia Bright","other":{"a":{"b":{"c":"yo"}}}},{"id":2,"name":"Marylou Caldwell","other":{"a":{"b":{"c":"yo"}}}}]""" + } + + @Test + fun parse_should_scan_to_get_the_first_and_second_from_last_items_of_all_sublists() { + JsonPath.parse(LARGE_JSON)!!.read("$[2]..[0, -2]") + .toString() shouldBe """["nulla","ut",{"id":0,"name":"Felecia Bright","other":{"a":{"b":{"c":"yo"}}}},{"name":"Maryanne Wiggins","other":{"a":{"b":{"c":"yo"}}}}]""" + } + + @Test + fun parse_should_scan_to_get_the_first_and_second_range_items_of_all_sublists() { + JsonPath.parse(LARGE_JSON)!!.read("$[2]..[0:2]") + .toString() shouldBe """["nulla","elit",{"id":0,"name":"Felecia Bright","other":{"a":{"b":{"c":"yo"}}}},{"name":"Maryanne Wiggins","other":{"a":{"b":{"c":"yo"}}}}]""" + } + + @Test + fun parse_should_scan_to_get_the_third_and_all_following_items_of_all_sublists() { + JsonPath.parse(LARGE_JSON)!!.read("$[2]..[2:]") + .toString() shouldBe """["ipsum","pariatur","ullamco","ut","sint",{"id":2,"name":"Marylou Caldwell","other":{"a":{"b":{"c":"yo"}}}}]""" + } + + @Test + fun parse_should_scan_to_get_all_items_except_for_last_item_of_all_sublists() { + JsonPath.parse(LARGE_JSON)!!.read("$[2]..[:-1]") + .toString() shouldBe """["nulla","elit","ipsum","pariatur","ullamco","ut",{"id":0,"name":"Felecia Bright","other":{"a":{"b":{"c":"yo"}}}},{"name":"Maryanne Wiggins","other":{"a":{"b":{"c":"yo"}}}}]""" + } + + @Test + fun parse_should_scan_to_get_all_items_starting_from_second_to_last_of_all_sublists() { + JsonPath.parse(LARGE_JSON)!!.read("$[2]..[-2:]") + .toString() shouldBe """["ut","sint",{"name":"Maryanne Wiggins","other":{"a":{"b":{"c":"yo"}}}},{"id":2,"name":"Marylou Caldwell","other":{"a":{"b":{"c":"yo"}}}}]""" + } + + @Test + fun parse_should_scan_to_get_all_items_between_first_and_last_of_all_sublists_both_sides_exclusive() { + JsonPath.parse(LARGE_JSON)!!.read("$[2]..[1:-1]") + .toString() shouldBe """["elit","ipsum","pariatur","ullamco","ut",{"name":"Maryanne Wiggins","other":{"a":{"b":{"c":"yo"}}}}]""" + } + + @Test + fun parse_should_scan_to_get_all_items_from_second_to_last_to_4th_item_of_all_sublists() { + JsonPath.parse(LARGE_JSON)!!.read("$[2]..[-2:5]") + .toString() shouldBe """[{"name":"Maryanne Wiggins","other":{"a":{"b":{"c":"yo"}}}},{"id":2,"name":"Marylou Caldwell","other":{"a":{"b":{"c":"yo"}}}}]""" + } + + @Test + fun parse_should_scan_to_get_second_from_last_item_of_all_sublists() { + JsonPath.parse(LARGE_JSON)!!.read("$[2]..[-2:-1]") + .toString() shouldBe """["ut",{"name":"Maryanne Wiggins","other":{"a":{"b":{"c":"yo"}}}}]""" + } + + @Test + fun parse_should_scan_to_keys_on_same_level_of_all_sublists() { + JsonPath.parse(LARGE_JSON)!!.read("$[2]..['name','id']") + .toString() shouldBe """["Marie Hampton","Felecia Bright",0,"Maryanne Wiggins","Marylou Caldwell",2]""" + } + + @Test + fun parse_should_scan_to_keys_on_different_level_of_all_sublists() { + JsonPath.parse(LARGE_JSON)!!.read("$[2]..['name','company','id']") + .toString() shouldBe """["Marie Hampton","ZENCO","Felecia Bright",0,"Maryanne Wiggins","Marylou Caldwell",2]""" + } + + @Test + fun parse_should_scan_to_get_all_items_after_the_first_of_all_sublists_even_if_end_out_of_range() { + JsonPath.parse(LARGE_JSON)!!.read("$[2]..[1:100]") + .toString() shouldBe """["elit","ipsum","pariatur","ullamco","ut","sint",{"name":"Maryanne Wiggins","other":{"a":{"b":{"c":"yo"}}}},{"id":2,"name":"Marylou Caldwell","other":{"a":{"b":{"c":"yo"}}}}]""" + } + + @Test + fun parse_should_get_the_key_of_every_2nd_item_in_all_sublists() { + val json = """ + { + "k": [{"key": "some value"}, {"key": 42}], + "kk": [[{"key": 100}, {"key": 200}, {"key": 300}], [{"key": 400}, {"key": 500}, {"key": 600}]], + "key": [0, 1] + } + """.trimIndent() + JsonPath.parse(json)!!.read("$..[1].key") + .toString() shouldBe """[42,200,500]""" + } +} diff --git a/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/path/ListCollectionsTest.kt b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/path/ListCollectionsTest.kt new file mode 100644 index 0000000..b34052b --- /dev/null +++ b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/path/ListCollectionsTest.kt @@ -0,0 +1,55 @@ +package com.nfeld.jsonpathkt.path + +import com.nfeld.jsonpathkt.JsonPath +import com.nfeld.jsonpathkt.LARGE_JSON +import com.nfeld.jsonpathkt.extension.read +import io.kotest.matchers.shouldBe +import kotlinx.serialization.json.JsonPrimitive +import kotlin.test.Test + +class ListCollectionsTest { + @Test + fun parse_should_include_nulls() { + JsonPath.parse("""{"key": [1, "random", null, 1.765]}""")!! + .read>("$.key") shouldBe listOf( + JsonPrimitive(1), + JsonPrimitive("random"), + null, + JsonPrimitive(1.765) + ) + } + + @Test + fun parse_should_be_String_collection() { + JsonPath.parse(LARGE_JSON)!!.read>("$[0].tags") shouldBe listOf( + "occaecat", + "mollit", + "ullamco", + "labore", + "cillum", + "laboris", + "qui" + ) + } + + @Test + fun parse_should_be_Int_collection() { + JsonPath.parse(LARGE_JSON)!!.read>("$[5].nums") shouldBe listOf(1, 2, 3, 4, 5) + } + + @Test + fun parse_should_be_Long_collection() { + JsonPath.parse(LARGE_JSON)!!.read>("$[5].nums") shouldBe listOf( + 1L, + 2L, + 3L, + 4L, + 5L + ) + } + + @Test + fun parse_should_get_a_Set_collection_to_remove_duplicates() { + JsonPath.parse("""[1,2,3,1,2,4,5]""")!!.read>("$") shouldBe setOf(1, 2, 3, 4, 5) + } +} diff --git a/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/path/MapObjectsTest.kt b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/path/MapObjectsTest.kt new file mode 100644 index 0000000..475bc68 --- /dev/null +++ b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/path/MapObjectsTest.kt @@ -0,0 +1,16 @@ +package com.nfeld.jsonpathkt.path + +import com.nfeld.jsonpathkt.JsonPath +import com.nfeld.jsonpathkt.extension.read +import io.kotest.matchers.shouldBe +import kotlin.test.Test + +class MapObjectsTest { + @Test + fun should_be_Map() { + JsonPath.parse("""{"a": {"b": "yo"}}""")!! + .read>>("$") shouldBe mapOf("a" to mapOf("b" to "yo")) + JsonPath.parse("""{"a": {"b": "yo"}}""")!! + .read>("$.a") shouldBe mapOf("b" to "yo") + } +} diff --git a/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/path/MultiArrayAccessorsTest.kt b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/path/MultiArrayAccessorsTest.kt new file mode 100644 index 0000000..1f0bbcb --- /dev/null +++ b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/path/MultiArrayAccessorsTest.kt @@ -0,0 +1,30 @@ +package com.nfeld.jsonpathkt.path + +import com.nfeld.jsonpathkt.JsonPath +import com.nfeld.jsonpathkt.LARGE_JSON +import com.nfeld.jsonpathkt.extension.read +import io.kotest.matchers.shouldBe +import kotlinx.serialization.json.JsonElement +import kotlin.test.Test + +class MultiArrayAccessorsTest { + @Test + fun parse_should_get_first_fourth_and_sixth_items() { + JsonPath.parse(LARGE_JSON)!!.read>("$[0]['tags'][0,3,5]") shouldBe listOf( + "occaecat", + "labore", + "laboris" + ) + } + + @Test + fun parse_should_get_only_the_items_with_valid_index() { + JsonPath.parse(LARGE_JSON)!! + .read>("$[0]['tags'][0,30,50]") shouldBe listOf("occaecat") + } + + @Test + fun parse_should_return_empty_list_if_used_on_JSON_object() { + JsonPath.parse("""{"key":3}""")!!.read("$[3,4]")?.toString() shouldBe "[]" + } +} diff --git a/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/path/MultiObjectAccessorsTest.kt b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/path/MultiObjectAccessorsTest.kt new file mode 100644 index 0000000..a9eecec --- /dev/null +++ b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/path/MultiObjectAccessorsTest.kt @@ -0,0 +1,51 @@ +package com.nfeld.jsonpathkt.path + +import com.nfeld.jsonpathkt.JsonPath +import com.nfeld.jsonpathkt.LARGE_JSON +import com.nfeld.jsonpathkt.extension.read +import io.kotest.matchers.shouldBe +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonPrimitive +import kotlin.test.Test + +class MultiObjectAccessorsTest { + @Test + fun parse_should_get_list_of_scalars() { + JsonPath.parse("""{"key": "value", "another": "entry"}""")!! + .read>("$['key','another']") shouldBe listOf("value", "entry") + } + + @Test + fun parse_should_return_empty_list_of_reading_from_a_list_that_is_not_root_list() { + JsonPath.parse("""[{"key": "value", "another": "entry"}]""")!! + .read("$['key','another']") + .toString() shouldBe "[]" + JsonPath.parse("""[{"key": "ey", "other": 1}, {"key": "bee"}, {"key": "see", "else": 3}]""")!! + .read("$['key','other']").toString() shouldBe "[]" + } + + @Test + fun parse_should_read_obj_keys_from_root_list() { + JsonPath.parse("""[{"key": "value", "another": "entry"}]""")!! + .read>("$.*['key','another']") shouldBe listOf("value", "entry") + JsonPath.parse("""[{"key": "ey", "other": 1}, {"key": "bee"}, {"key": "see", "else": 3}]""")!! + .read("$.*['key','other']").toString() shouldBe """["ey",1,"bee","see"]""" + } + + @Test + fun parse_should_get_all_3_keys() { + JsonPath.parse(LARGE_JSON)!! + .read>("$[0]['latitude','longitude','isActive']") shouldBe listOf( + JsonPrimitive(-85.888651), JsonPrimitive(38.287152), JsonPrimitive(true) + ) + } + + @Test + fun parse_should_get_only_the_key_value_pairs_when_found() { + JsonPath.parse(LARGE_JSON)!! + .read>("$[0]['latitude','longitude', 'unknownkey']") shouldBe listOf( + -85.888651, + 38.287152 + ) + } +} diff --git a/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/path/ObjectAccessorsTest.kt b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/path/ObjectAccessorsTest.kt new file mode 100644 index 0000000..899c880 --- /dev/null +++ b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/path/ObjectAccessorsTest.kt @@ -0,0 +1,99 @@ +package com.nfeld.jsonpathkt.path + +import com.nfeld.jsonpathkt.JsonPath +import com.nfeld.jsonpathkt.SMALL_JSON +import com.nfeld.jsonpathkt.extension.read +import io.kotest.matchers.shouldBe +import kotlinx.serialization.json.JsonElement +import kotlin.test.Test + +class ObjectAccessorsTest { + @Test + fun parse_should_be_null_if_key_does_not_exist() { + JsonPath.parse(SMALL_JSON)!!.read("$.unknownkey") shouldBe null + JsonPath.parse(SMALL_JSON)!!.read("$['unknownkey']") shouldBe null + } + + @Test + fun parse_should_get_value_if_key_exists() { + JsonPath.parse(SMALL_JSON)!!.read("$.key") shouldBe 5 + JsonPath.parse(SMALL_JSON)!!.read("$['key']") shouldBe 5 + } + + @Test + fun parse_should_be_null_if_reading_null_value() { + JsonPath.parse("""{"key":null}""")!!.read("$['key']") shouldBe null + } + + @Test + fun parse_should_access_empty_string_key_and_other_uncommon_keys() { + JsonPath.parse("""{"":4}""")!!.read("$['']") shouldBe 4 + JsonPath.parse("""{"":4}""")!!.read("$[\"\"]") shouldBe 4 + JsonPath.parse("""{"'":4}""")!!.read("$[\"'\"]") shouldBe 4 + JsonPath.parse("""{"'":4}""")!!.read("$['\\'']") shouldBe 4 + JsonPath.parse("""{"\"": 4}""")!!.read("""$["\""]""") shouldBe 4 + JsonPath.parse("""{"\"": 4}""")!!.read("""$['"']""") shouldBe 4 + JsonPath.parse("""{"\\": 4}""")!!.read("""$['\\']""") shouldBe 4 + } + + @Test + fun parse_should_read_object_keys_that_have_numbers_and_or_symbols() { + val key = "!@#\$%^&*()_-+=[]{}|:;<,>.?`~" // excluding ' + val json = """ + { + "key1": "a", + "ke2y": "b", + "ke3%y": "c", + "1234": "d", + "12$34": "e", + "abc{}3d": "f", + "$key": "g" + } + """ + JsonPath.parse(json)!!.read("$.key1") shouldBe "a" + JsonPath.parse(json)!!.read("$['key1']") shouldBe "a" + JsonPath.parse(json)!!.read("$.ke2y") shouldBe "b" + JsonPath.parse(json)!!.read("$['ke2y']") shouldBe "b" + JsonPath.parse(json)!!.read("$.ke3%y") shouldBe "c" + JsonPath.parse(json)!!.read("$['ke3%y']") shouldBe "c" + JsonPath.parse(json)!!.read("$.1234") shouldBe "d" + JsonPath.parse(json)!!.read("$['1234']") shouldBe "d" + JsonPath.parse(json)!!.read("$.12$34") shouldBe "e" + JsonPath.parse(json)!!.read("$['12$34']") shouldBe "e" + JsonPath.parse(json)!!.read("$.abc{}3d") shouldBe "f" + JsonPath.parse(json)!!.read("$['abc{}3d']") shouldBe "f" + JsonPath.parse(json)!!.read("$['$key']") shouldBe "g" + } + + @Test + fun parse_should_be_null_on_unsupported_selectors_on_objects() { + JsonPath.parse(SMALL_JSON)!!.read("$[:]") shouldBe null + } + + @Test + fun parse_should_read_key_from_list_if_list_item_is_an_object() { + JsonPath.parse("""[{"key": "ey"}, {"key": "bee"}, {"key": "see"}]""")!! + .read("$.key") shouldBe null + JsonPath.parse("""[{"key": "ey"}, {"key": "bee"}, {"key": "see"}]""")!! + .read("$.*.key") + .toString() shouldBe """["ey","bee","see"]""" + JsonPath.parse("""[{"key": "ey"}, {"key": "bee"}, {"key": "see"}]""")!! + .read("$[0,2].key") + .toString() shouldBe """["ey","see"]""" + JsonPath.parse( + """ + { + "one": {"key": "value"}, + "two": {"k": "v"}, + "three": {"some": "more", "key": "other value"} + } + """ + )!!.read("$['one','three'].key") + .toString() shouldBe """["value","other value"]""" + + JsonPath.parse("""[{"a": 1},{"a": 1}]""")!!.read("$[*].a") + .toString() shouldBe """[1,1]""" + JsonPath.parse("""[{"a": 1},{"a": 1}]""")!!.read("$.*.a") + .toString() shouldBe """[1,1]""" + } +} diff --git a/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/path/TypecastingTest.kt b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/path/TypecastingTest.kt new file mode 100644 index 0000000..99b7627 --- /dev/null +++ b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/path/TypecastingTest.kt @@ -0,0 +1,33 @@ +package com.nfeld.jsonpathkt.path + +import com.nfeld.jsonpathkt.JsonPath +import com.nfeld.jsonpathkt.extension.read +import io.kotest.matchers.shouldBe +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonPrimitive +import kotlin.test.Test + +@Serializable +data class PojoClass1(val key: String) + +@Serializable +data class PojoClass2(val key: List) + +@Serializable +data class PojoClass3(val key: Map, val default: Int) + +class TypeCastingTest { + @Test + fun parse_should_cast_pojo() { + JsonPath.parse("""{"key": "value"}""")!!.read("$") shouldBe PojoClass1(key = "value") + JsonPath.parse("""{"key": [1, "random", null, 1.765]}""")!!.read("$") shouldBe PojoClass2( + key = listOf( + JsonPrimitive(1), JsonPrimitive("random"), null, JsonPrimitive(1.765) + ) + ) + JsonPath.parse("""{"key": { "a": 1, "b": 2 }, "default": 3}""")!!.read("$") shouldBe PojoClass3( + key = mapOf("a" to 1, "b" to 2), + default = 3 + ) + } +} diff --git a/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/path/WildcardTest.kt b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/path/WildcardTest.kt new file mode 100644 index 0000000..67d185c --- /dev/null +++ b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/path/WildcardTest.kt @@ -0,0 +1,149 @@ +package com.nfeld.jsonpathkt.path + +import com.nfeld.jsonpathkt.JsonPath +import com.nfeld.jsonpathkt.LARGE_JSON +import com.nfeld.jsonpathkt.extension.read +import io.kotest.matchers.shouldBe +import kotlinx.serialization.json.JsonElement +import kotlin.test.Test + +class WildcardTest { + @Test + fun parse_should_handle_all_types_of_reads_correctly() { + // we have obj, list, list, and scalars. It should handle them all correctly the further we go down with wildcards + // keep in mind wildcards always return a list, and drop scalars when going up a list. Scalars dont have levels to go up, only container nodes do + + JsonPath.parse("""[{"bar": [42]}]""")!!.read("$.*") + .toString() shouldBe """[{"bar":[42]}]""" // in root list + JsonPath.parse("""[{"bar": [42]}]""")!!.read("$.*.*") + .toString() shouldBe """[[42]]""" // root list wrapped around item from next level + JsonPath.parse("""[{"bar": [42]}]""")!!.read("$.*.*.*") + .toString() shouldBe """[42]""" // root list wrapped around item from next level + JsonPath.parse("""[{"bar": [42]}]""")!!.read("$.*.*.*.*") + .toString() shouldBe "[]" // root list since wildcard always returns a list + + JsonPath.parse("""[{"bar": [42]},{"bae": [30]}]""")!!.read("$.*") + .toString() shouldBe """[{"bar":[42]},{"bae":[30]}]""" // in root list + JsonPath.parse("""[{"bar": [42]},{"bae": [30]}]""")!!.read("$.*.*") + .toString() shouldBe """[[42],[30]]""" // root list wrapped around items from next level + JsonPath.parse("""[{"bar": [42]},{"bae": [30]}]""")!!.read("$.*.*.*") + .toString() shouldBe """[42,30]""" // root list wrapped around items from next level + JsonPath.parse("""[{"bar": [42]},{"bae": [30]}]""")!!.read("$.*.*.*.*") + .toString() shouldBe "[]" // root list since wildcard always returns a list + + JsonPath.parse("""[{"bar": [42]},{"bae": [30,31]}]""")!!.read("$.*") + .toString() shouldBe """[{"bar":[42]},{"bae":[30,31]}]""" // in root list + JsonPath.parse("""[{"bar": [42]},{"bae": [30,31]}]""")!!.read("$.*.*") + .toString() shouldBe """[[42],[30,31]]""" // root list wrapped around items from next level + JsonPath.parse("""[{"bar": [42]},{"bae": [30,31]}]""")!!.read("$.*.*.*") + .toString() shouldBe """[42,30,31]""" // root list wrapped around items from next level + JsonPath.parse("""[{"bar": [42]},{"bae": [30,31]}]""")!!.read("$.*.*.*.*") + .toString() shouldBe "[]" // root list since wildcard always returns a list + + JsonPath.parse("""[{"bar": [42], "scalarkey": "scalar2"}, "scalar1"]""")!! + .read("$.*") + .toString() shouldBe """[{"bar":[42],"scalarkey":"scalar2"},"scalar1"]""" // in root list + JsonPath.parse("""[{"bar": [42], "scalarkey": "scalar2"}, "scalar1"]""")!! + .read("$.*.*") + .toString() shouldBe """[[42],"scalar2"]""" // root list wrapped around items from next level + JsonPath.parse("""[{"bar": [42], "scalarkey": "scalar2"}, "scalar1"]""")!! + .read("$.*.*.*") + .toString() shouldBe """[42]""" // root list wrapped around item from next level + JsonPath.parse("""[{"bar": [42], "scalarkey": "scalar2"}, "scalar1"]""")!! + .read("$.*.*.*.*") + .toString() shouldBe "[]" // root list since wildcard always returns a list + + JsonPath.parse("""[[1], [2,3]]""")!!.read("$.*") + .toString() shouldBe """[[1],[2,3]]""" + JsonPath.parse("""[[1], [2,3]]""")!!.read("$.*.*") + .toString() shouldBe """[1,2,3]""" + JsonPath.parse("""[[1], [2,3]]""")!!.read("$.*.*.*") + .toString() shouldBe """[]""" + + JsonPath.parse("""[1,[2],[3,4],[5,6,7,[8,9,10,11]]]""")!!.read("$.*") + .toString() shouldBe """[1,[2],[3,4],[5,6,7,[8,9,10,11]]]""" + JsonPath.parse("""[1,[2],[3,4],[5,6,7,[8,9,10,11]]]""")!!.read("$.*.*") + .toString() shouldBe """[2,3,4,5,6,7,[8,9,10,11]]""" + JsonPath.parse("""[1,[2],[3,4],[5,6,7,[8,9,10,11]]]""")!!.read("$.*.*.*") + .toString() shouldBe """[8,9,10,11]""" + JsonPath.parse("""[1,[2],[3,4],[5,6,7,[8,9,10,11]]]""")!!.read("$.*.*.*.*") + .toString() shouldBe """[]""" + + JsonPath.parse("""[1,[2],[3,4],[5,6,7,[8,9,10,11]]]""")!!.read("$..*") + .toString() shouldBe """[1,[2],[3,4],[5,6,7,[8,9,10,11]],2,3,4,5,6,7,[8,9,10,11],8,9,10,11]""" + JsonPath.parse("""[1,[2],[3,4],[5,6,7,[8,9,10,11]]]""")!!.read("$..[*]") + .toString() shouldBe """[1,[2],[3,4],[5,6,7,[8,9,10,11]],2,3,4,5,6,7,[8,9,10,11],8,9,10,11]""" + JsonPath.parse("""[1,[2],[3,4],[5,6,7,[8,9,10,11]]]""")!!.read("$..*.*") + .toString() shouldBe """[2,3,4,5,6,7,[8,9,10,11],8,9,10,11]""" + JsonPath.parse("""[1,[2],[3,4],[5,6,7,[8,9,10,11]]]""")!!.read("$..*..*") + .toString() shouldBe """[2,3,4,5,6,7,[8,9,10,11],8,9,10,11,8,9,10,11]""" + } + + @Test + fun parse_should_handle_lists_properly() { + // Returns list held by "bar", wrapped in the root level list since wildcard always returns list + JsonPath.parse("""[{"bar": [42]}]""")!!.read("$[*].bar") + .toString() shouldBe """[[42]]""" + // Returns root level list now with the item from the inner list. + JsonPath.parse("""[{"bar": [42]}]""")!!.read("$.*.bar.*") + .toString() shouldBe """[42]""" + JsonPath.parse("""[{"bar": [42]}]""")!!.read("$[*].bar[*]") + .toString() shouldBe """[42]""" + // This would be a wildcard on a root level list which removes the scalars on that level. It should be list, not null as wildcard always returns list + JsonPath.parse("""[{"bar": [42]}]""")!!.read("$[*].bar[*][*]") + .toString() shouldBe """[]""" + + JsonPath.parse("""[[1], [2,3]]""")!!.read("$.*[0]") + .toString() shouldBe """[1,2]""" // first item of each sublist in root + JsonPath.parse("""[[1], [2,3]]""")!!.read("$.*[1]") + .toString() shouldBe """[3]""" // second item of each sublist in root, which there is only 1 of + JsonPath.parse("""[[1], [2,3]]""")!!.read("$.*[1].*") + .toString() shouldBe """[]""" // second item of each sublist in root, which there is only 1 of + + println(JsonPath.parse(LARGE_JSON)!!.read("$..friends").toString()) + println(JsonPath.parse(LARGE_JSON)!!.read("$..friends..name").toString()) + println(JsonPath.parse(LARGE_JSON)!!.read("$..friends..name[1]").toString()) + JsonPath.parse(LARGE_JSON)!!.read("$..friends.*.name") + .toString() shouldBe """["Kathrine Osborn","Vonda Howe","Harrell Pratt","Mason Leach","Spencer Valenzuela","Hope Medina","Felecia Bright","Maryanne Wiggins","Marylou Caldwell","Rios Norton","Judy Good","Rosetta Stanley","Lora Cotton","Gaines Henry","Dorothea Irwin"]""" + JsonPath.parse(LARGE_JSON)!!.read>("$..friends.[*].name") shouldBe listOf( + "Kathrine Osborn", + "Vonda Howe", + "Harrell Pratt", + "Mason Leach", + "Spencer Valenzuela", + "Hope Medina", + "Felecia Bright", + "Maryanne Wiggins", + "Marylou Caldwell", + "Rios Norton", + "Judy Good", + "Rosetta Stanley", + "Lora Cotton", + "Gaines Henry", + "Dorothea Irwin" + ) + } + + @Test + fun parse_should_return_null_if_null_read_before_wildcard() { + JsonPath.parse("{}")!!.read("$.key.*") shouldBe null + JsonPath.parse("{}")!!.read("$.key[*]") shouldBe null + } + + @Test + fun parse_should_return_self_if_used_on_scalar() { + JsonPath.parse("5")!!.read("$.*") shouldBe 5 + JsonPath.parse("5.34")!!.read("$.*") shouldBe 5.34 + JsonPath.parse("true")!!.read("$.*") shouldBe true + JsonPath.parse("false")!!.read("$.*") shouldBe false + JsonPath.parse(""""hello"""")!!.read("$.*") shouldBe "hello" + } + + @Test + fun parse_should_get_the_values_of_the_JSON_object() { + JsonPath.parse(LARGE_JSON)!!.read("$..friends[-1].*") + .toString() shouldBe """[2,"Harrell Pratt",{"a":{"b":{"c":"yo"}}},2,"Hope Medina",{"a":{"b":{"c":"yo"}}},2,"Marylou Caldwell",{"a":{"b":{"c":"yo"}}},2,"Rosetta Stanley",{"a":{"b":{"c":"yo"}}},2,"Dorothea Irwin",{"a":{"b":{"c":"yo"}}}]""" + JsonPath.parse(LARGE_JSON)!!.read("$..friends[-1][*]") + .toString() shouldBe """[2,"Harrell Pratt",{"a":{"b":{"c":"yo"}}},2,"Hope Medina",{"a":{"b":{"c":"yo"}}},2,"Marylou Caldwell",{"a":{"b":{"c":"yo"}}},2,"Rosetta Stanley",{"a":{"b":{"c":"yo"}}},2,"Dorothea Irwin",{"a":{"b":{"c":"yo"}}}]""" + } +} diff --git a/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/test_utils.kt b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/test_utils.kt new file mode 100644 index 0000000..832a08f --- /dev/null +++ b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/test_utils.kt @@ -0,0 +1,16 @@ +package com.nfeld.jsonpathkt + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject + +internal fun printTesting(subpath: String) { + println("Testing like $subpath") +} + +internal fun emptyJsonObject() = JsonObject(emptyMap()) +internal fun emptyJsonArray() = JsonArray(emptyList()) +internal fun JsonElement.toJsonNode(isNewRoot: Boolean = false) = JsonNode(element = this, isNewRoot = isNewRoot) + +fun readTree(json: String): JsonElement = Json.parseToJsonElement(json) diff --git a/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/tokens/ArrayAccessorTokenTest.kt b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/tokens/ArrayAccessorTokenTest.kt new file mode 100644 index 0000000..30337fc --- /dev/null +++ b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/tokens/ArrayAccessorTokenTest.kt @@ -0,0 +1,67 @@ +package com.nfeld.jsonpathkt.tokens + +import com.nfeld.jsonpathkt.emptyJsonObject +import com.nfeld.jsonpathkt.readTree +import com.nfeld.jsonpathkt.toJsonNode +import io.kotest.matchers.shouldBe +import kotlinx.serialization.json.JsonArray +import kotlin.test.Test + +class ArrayAccessorTokenTest { + @Test + fun should_be_null_if_item_does_not_exist_at_index() { + ArrayAccessorToken(0).read(emptyJsonObject().toJsonNode()) shouldBe null + } + + @Test + fun should_get_the_item_if_it_exists_at_index() { + ArrayAccessorToken(0).read(readTree("[1,2]").toJsonNode())?.element.toString() shouldBe "1" + } + + @Test + fun should_get_the_item_if_it_exists_at_index_if_negative() { + ArrayAccessorToken(-1).read(readTree("[1,2]").toJsonNode())?.element.toString() shouldBe "2" + } + + @Test + fun should_get_last_item() { + ArrayAccessorToken(-1).read(readTree("[1,2]").toJsonNode())?.element.toString() shouldBe "2" + } + + @Test + fun should_be_null_if_node_is_an_JsonObject() { + ArrayAccessorToken(0).read(readTree("""{"0":1}""").toJsonNode()) shouldBe null + } + + @Test + fun should_get_item_if_node_is_a_New_Root() { + val rootJson = readTree("[[1]]") as JsonArray + ArrayAccessorToken(0).read(rootJson.toJsonNode(isNewRoot = true))?.element.toString() shouldBe "[1]" // list since it was root level + } + + @Test + fun should_get_first_item_of_sublists_if_node_is_a_New_Root() { + val rootJson = readTree("[1,[2],[3,4],[5,6,7]]") as JsonArray + ArrayAccessorToken(0).read(rootJson.toJsonNode(isNewRoot = true))?.element.toString() shouldBe "[2,3,5]" + } + + @Test + fun should_get_last_item_of_sublists_if_node_is_a_New_Root() { + val rootJson = readTree("[1,[2],[3,4],[5,6,7]]") as JsonArray + ArrayAccessorToken(-1).read(rootJson.toJsonNode(isNewRoot = true))?.element.toString() shouldBe "[2,4,7]" + } + + @Test + fun should_get_character_of_a_String_at_specified_index() { + ArrayAccessorToken(1).read(readTree("\"hello\"").toJsonNode())?.element.toString() shouldBe "\"e\"" + ArrayAccessorToken(-1).read(readTree("\"hello\"").toJsonNode())?.element.toString() shouldBe "\"o\"" + ArrayAccessorToken(-8).read(readTree("\"hello\"").toJsonNode()) shouldBe null // out of bounds + } + + @Test + fun should_get_specified_character_of_every_String_in_a_root_level_array() { + ArrayAccessorToken(1).read(WildcardToken().read(readTree("""["hello","world"]""").toJsonNode()))?.element.toString() shouldBe """["e","o"]""" + ArrayAccessorToken(-1).read(WildcardToken().read(readTree("""["hello","world"]""").toJsonNode()))?.element.toString() shouldBe """["o","d"]""" + ArrayAccessorToken(-4).read(WildcardToken().read(readTree("""["h","world"]""").toJsonNode()))?.element.toString() shouldBe """["o"]""" + } +} diff --git a/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/tokens/ArrayLengthBasedRangeAccessorTokenTest.kt b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/tokens/ArrayLengthBasedRangeAccessorTokenTest.kt new file mode 100644 index 0000000..21dd23d --- /dev/null +++ b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/tokens/ArrayLengthBasedRangeAccessorTokenTest.kt @@ -0,0 +1,140 @@ +package com.nfeld.jsonpathkt.tokens + +import com.nfeld.jsonpathkt.emptyJsonObject +import com.nfeld.jsonpathkt.printTesting +import com.nfeld.jsonpathkt.readTree +import com.nfeld.jsonpathkt.toJsonNode +import io.kotest.matchers.shouldBe +import kotlinx.serialization.json.JsonArray +import kotlin.test.Test +import kotlin.test.assertEquals + +class ArrayLengthBasedRangeAccessorTokenTest { + @Test + fun should_return_empty_list() { + ArrayLengthBasedRangeAccessorToken(0).read(emptyJsonObject().toJsonNode()).element.toString() shouldBe "[]" + } + + @Test + fun should_not_get_characters_of_a_String() { + ArrayLengthBasedRangeAccessorToken(1).read(readTree("\"hello\"").toJsonNode()).element.toString() shouldBe "[]" + } + + @Test + fun should_not_get_characters_of_every_String_in_a_root_level_array() { + ArrayLengthBasedRangeAccessorToken( + 0, + 2 + ).read(WildcardToken().read(readTree("""["hello","world"]""").toJsonNode())).element.toString() shouldBe "[]" + ArrayLengthBasedRangeAccessorToken( + 2, + null, + -1 + ).read(WildcardToken().read(readTree("""["hello","world"]""").toJsonNode())).element.toString() shouldBe "[]" + } + + @Test + fun should_handle_objects_in_a_root_level_array() { + ArrayLengthBasedRangeAccessorToken( + 0, + 1 + ).read(WildcardToken().read(readTree("""[{"a":1,"b":{"c":2,"d":3},"e":4}]""").toJsonNode())).element.toString() shouldBe "[]" + ArrayLengthBasedRangeAccessorToken( + 0, + -1 + ).read(WildcardToken().read(readTree("""[{"a":1,"b":{"c":2,"d":3},"e":4}]""").toJsonNode())).element.toString() shouldBe "[]" + ArrayLengthBasedRangeAccessorToken( + 0, + -1 + ).read(WildcardToken().read(readTree("""[{"p":true},{"a":1,"b":{"c":2,"d":3},"e":4}]""").toJsonNode())).element.toString() shouldBe "[]" + } + + @Test + fun should_handle_different_levels_of_list_nesting() { + ArrayLengthBasedRangeAccessorToken( + 0, + null, + -1 + ).read(readTree("""[1,[2],[3,4],[5,6,7]]""").toJsonNode()).element.toString() shouldBe "[1,[2],[3,4]]" + ArrayLengthBasedRangeAccessorToken( + 0, + null, + 0 + ).read(readTree("""[1,[2],[3,4],[5,6,7]]""").toJsonNode()).element.toString() shouldBe "[1,[2],[3,4],[5,6,7]]" + ArrayLengthBasedRangeAccessorToken(0).read(WildcardToken().read(readTree("""[1,[2],[3,4],[5,6,7]]""").toJsonNode())).element.toString() shouldBe "[2,3,4,5,6,7]" + ArrayLengthBasedRangeAccessorToken( + 0, + null, + -1 + ).read(WildcardToken().read(readTree("""[1,[2],[3,4],[5,6,7]]""").toJsonNode())).element.toString() shouldBe "[3,5,6]" + ArrayLengthBasedRangeAccessorToken( + 0, + null, + 0 + ).read(WildcardToken().read(readTree("""[1,[2],[3,4],[5,6,7,[8,9,10,11]]]""").toJsonNode())).element.toString() shouldBe "[2,3,4,5,6,7,[8,9,10,11]]" + } + + @Test + fun to_MultiArrayAccessorToken_general_cases() { + val json = readTree("[0,1,2,3,4]") as JsonArray + + printTesting("[0:]") + var res = ArrayLengthBasedRangeAccessorToken(0, null, 0).toMultiArrayAccessorToken(json) + var expected = MultiArrayAccessorToken(listOf(0, 1, 2, 3, 4)) + assertEquals(expected, res) + + printTesting("[3:]") + res = ArrayLengthBasedRangeAccessorToken(3, null, 0).toMultiArrayAccessorToken(json) + expected = MultiArrayAccessorToken(listOf(3, 4)) + assertEquals(expected, res) + + printTesting("[:-1]") + res = ArrayLengthBasedRangeAccessorToken(0, null, -1).toMultiArrayAccessorToken(json) + expected = MultiArrayAccessorToken( + listOf( + 0, + 1, + 2, + 3 + ) + ) // this kind of range has end exclusive, so not really to end + assertEquals(expected, res) + + // test starting edge + printTesting("[:-4]") + res = ArrayLengthBasedRangeAccessorToken(0, null, -4).toMultiArrayAccessorToken(json) + expected = MultiArrayAccessorToken(listOf(0)) + assertEquals(expected, res) + + // test ending edge + printTesting("[-1:]") + res = ArrayLengthBasedRangeAccessorToken(-1, null, 0).toMultiArrayAccessorToken(json) + expected = MultiArrayAccessorToken(listOf(4)) + assertEquals(expected, res) + + printTesting("[-2:]") + res = ArrayLengthBasedRangeAccessorToken(-2, null, 0).toMultiArrayAccessorToken(json) + expected = MultiArrayAccessorToken(listOf(3, 4)) + assertEquals(expected, res) + + printTesting("[-4:-1]") + res = ArrayLengthBasedRangeAccessorToken(-4, null, -1).toMultiArrayAccessorToken(json) + expected = MultiArrayAccessorToken(listOf(1, 2, 3)) + assertEquals(expected, res) + + printTesting("[-4:4]") + res = ArrayLengthBasedRangeAccessorToken(-4, 4, 0).toMultiArrayAccessorToken(json) + expected = MultiArrayAccessorToken(listOf(1, 2, 3)) + assertEquals(expected, res) + + printTesting("[2:-1]") + res = ArrayLengthBasedRangeAccessorToken(2, null, -1).toMultiArrayAccessorToken(json) + expected = MultiArrayAccessorToken(listOf(2, 3)) + assertEquals(expected, res) + + printTesting("[:]") + res = ArrayLengthBasedRangeAccessorToken(0, null, 0).toMultiArrayAccessorToken(json) + expected = MultiArrayAccessorToken(listOf(0, 1, 2, 3, 4)) + assertEquals(expected, res) + } +} diff --git a/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/tokens/DeepScanArrayAccessorTokenTest.kt b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/tokens/DeepScanArrayAccessorTokenTest.kt new file mode 100644 index 0000000..5a208c1 --- /dev/null +++ b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/tokens/DeepScanArrayAccessorTokenTest.kt @@ -0,0 +1,76 @@ +package com.nfeld.jsonpathkt.tokens + +import com.nfeld.jsonpathkt.FAMILY_JSON +import com.nfeld.jsonpathkt.readTree +import com.nfeld.jsonpathkt.toJsonNode +import io.kotest.matchers.shouldBe +import kotlin.test.Test + +class DeepScanArrayAccessorTokenTest { + @Test + fun should_scan_for_indices() { + DeepScanArrayAccessorToken(listOf(0)).read(readTree(FAMILY_JSON).toJsonNode()).element.toString() shouldBe """[{"name":"Thomas","age":13}]""" + DeepScanArrayAccessorToken( + listOf( + 0, + 2 + ) + ).read(readTree(FAMILY_JSON).toJsonNode()).element.toString() shouldBe """[{"name":"Thomas","age":13},{"name":"Konstantin","age":29,"nickname":"Kons"}]""" + } + + @Test + fun results_should_be_a_New_Root() { + DeepScanArrayAccessorToken(listOf(0)).read(readTree(FAMILY_JSON).toJsonNode()).isNewRoot shouldBe true + } + + @Test + fun should_handle_different_nested_lists() { + val json = readTree("""[ {"a":1}, {"b":2}, [0,1,2, [ true, false ]] ]""") + DeepScanArrayAccessorToken(listOf(0)).read(json.toJsonNode()).element.toString() shouldBe """[{"a":1},0,true]""" + DeepScanArrayAccessorToken( + listOf( + 0, + 1 + ) + ).read(json.toJsonNode()).element.toString() shouldBe """[{"a":1},{"b":2},0,1,true,false]""" + + DeepScanArrayAccessorToken(listOf(0)).read(readTree("""[1,[2],[3,4],[5,6,7]]""").toJsonNode()).element.toString() shouldBe "[1,2,3,5]" + DeepScanArrayAccessorToken( + listOf( + 0, + 1 + ) + ).read(readTree("""[1,[2],[3,4],[5,6,7]]""").toJsonNode()).element.toString() shouldBe "[1,[2],2,3,4,5,6]" + DeepScanArrayAccessorToken( + listOf( + 0, + -1 + ) + ).read(readTree("""[1,[2],[3,4],[5,6,7]]""").toJsonNode()).element.toString() shouldBe "[1,[5,6,7],2,2,3,4,5,7]" + DeepScanArrayAccessorToken(listOf(0)).read(WildcardToken().read(readTree("""[1,[2],[3,4],[5,6,7]]""").toJsonNode())).element.toString() shouldBe "[2,3,5]" + DeepScanArrayAccessorToken( + listOf( + 0, + 1 + ) + ).read(WildcardToken().read(readTree("""[1,[2],[3,4],[5,6,7]]""").toJsonNode())).element.toString() shouldBe "[2,3,4,5,6]" + DeepScanArrayAccessorToken( + listOf( + 0, + -1 + ) + ).read(WildcardToken().read(readTree("""[1,[2],[3,4],[5,6,7]]""").toJsonNode())).element.toString() shouldBe "[2,2,3,4,5,7]" + DeepScanArrayAccessorToken( + listOf( + 0, + 1 + ) + ).read(WildcardToken().read(readTree("""[1,[2],[3,4],[5,6,7,[8,9,10,11]]]""").toJsonNode())).element.toString() shouldBe "[2,3,4,5,6,8,9]" + DeepScanArrayAccessorToken( + listOf( + 0, + -1 + ) + ).read(WildcardToken().read(readTree("""[1,[2],[3,4],[5,6,7,[8,9,10,11]]]""").toJsonNode())).element.toString() shouldBe "[2,2,3,4,5,[8,9,10,11],8,11]" + } +} diff --git a/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/tokens/DeepScanLengthBasedArrayAccessorTokenTest.kt b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/tokens/DeepScanLengthBasedArrayAccessorTokenTest.kt new file mode 100644 index 0000000..247e318 --- /dev/null +++ b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/tokens/DeepScanLengthBasedArrayAccessorTokenTest.kt @@ -0,0 +1,101 @@ +package com.nfeld.jsonpathkt.tokens + +import com.nfeld.jsonpathkt.printTesting +import com.nfeld.jsonpathkt.readTree +import com.nfeld.jsonpathkt.toJsonNode +import io.kotest.matchers.shouldBe +import kotlinx.serialization.json.JsonArray +import kotlin.test.Test +import kotlin.test.assertEquals + +class DeepScanLengthBasedArrayAccessorTokenTest { + @Test + fun should_handle_general_cases() { + val json = readTree("[0,1,2,3,4]") as JsonArray + + printTesting("[0:]") + var res = DeepScanLengthBasedArrayAccessorToken( + 0, + null, + 0 + ).read(json.toJsonNode()).element.toString() + assertEquals(json.toString(), res) + + printTesting("[1:]") + res = DeepScanLengthBasedArrayAccessorToken( + 1, + null, + 0 + ).read(json.toJsonNode()).element.toString() + assertEquals("[1,2,3,4]", res) + + printTesting("[:-2]") + res = DeepScanLengthBasedArrayAccessorToken( + 0, + null, + -2 + ).read(json.toJsonNode()).element.toString() + assertEquals("[0,1,2]", res) + + printTesting("[-3:]") + res = DeepScanLengthBasedArrayAccessorToken( + -3, + null, + 0 + ).read(json.toJsonNode()).element.toString() + assertEquals("[2,3,4]", res) + + printTesting("[0:-2]") + res = DeepScanLengthBasedArrayAccessorToken( + 0, + null, + -2 + ).read(json.toJsonNode()).element.toString() + assertEquals("[0,1,2]", res) + + printTesting("[-4:3]") + res = DeepScanLengthBasedArrayAccessorToken( + -4, + 3, + 0 + ).read(json.toJsonNode()).element.toString() + assertEquals("[1,2]", res) + + printTesting("[-3:-1]") + res = DeepScanLengthBasedArrayAccessorToken( + -3, + null, + -1 + ).read(json.toJsonNode()).element.toString() + assertEquals("[2,3]", res) + } + + @Test + fun should_handle_different_levels_of_list_nesting() { + DeepScanLengthBasedArrayAccessorToken( + 0, + null, + 0 + ).read(readTree("""[1,[2],[3,4],[5,6,7]]""").toJsonNode()).element.toString() shouldBe "[1,[2],[3,4],[5,6,7],2,3,4,5,6,7]" + DeepScanLengthBasedArrayAccessorToken( + 0, + null, + -1 + ).read(readTree("""[1,[2],[3,4],[5,6,7]]""").toJsonNode()).element.toString() shouldBe "[1,[2],[3,4],3,5,6]" + DeepScanLengthBasedArrayAccessorToken( + 0, + null, + 0 + ).read(WildcardToken().read(readTree("""[1,[2],[3,4],[5,6,7]]""").toJsonNode())).element.toString() shouldBe "[2,3,4,5,6,7]" + DeepScanLengthBasedArrayAccessorToken( + 0, + null, + -1 + ).read(WildcardToken().read(readTree("""[1,[2],[3,4],[5,6,7]]""").toJsonNode())).element.toString() shouldBe "[3,5,6]" + DeepScanLengthBasedArrayAccessorToken( + 0, + null, + 0 + ).read(WildcardToken().read(readTree("""[1,[2],[3,4],[5,6,7,[8,9,10,11]]]""").toJsonNode())).element.toString() shouldBe "[2,3,4,5,6,7,[8,9,10,11],8,9,10,11]" + } +} diff --git a/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/tokens/DeepScanObjectAccessorTokenTest.kt b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/tokens/DeepScanObjectAccessorTokenTest.kt new file mode 100644 index 0000000..720373c --- /dev/null +++ b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/tokens/DeepScanObjectAccessorTokenTest.kt @@ -0,0 +1,53 @@ +package com.nfeld.jsonpathkt.tokens + +import com.nfeld.jsonpathkt.FAMILY_JSON +import com.nfeld.jsonpathkt.readTree +import com.nfeld.jsonpathkt.toJsonNode +import io.kotest.matchers.shouldBe +import kotlin.test.Test + +class DeepScanObjectAccessorTokenTest { + @Test + fun should_scan_for_keys() { + DeepScanObjectAccessorToken(listOf("name")).read(readTree(FAMILY_JSON).toJsonNode()).element.toString() shouldBe """["Thomas","Mila","Konstantin","Tracy"]""" + DeepScanObjectAccessorToken(listOf("nickname")).read(readTree(FAMILY_JSON).toJsonNode()).element.toString() shouldBe """["Kons"]""" + DeepScanObjectAccessorToken( + listOf( + "name", + "age" + ) + ).read(readTree(FAMILY_JSON).toJsonNode()).element.toString() shouldBe """["Thomas",13,"Mila",18,"Konstantin",29,"Tracy",4]""" + DeepScanObjectAccessorToken( + listOf( + "name", + "nickname" + ) + ).read(readTree(FAMILY_JSON).toJsonNode()).element.toString() shouldBe """["Thomas","Mila","Konstantin","Kons","Tracy"]""" + } + + @Test + fun results_should_be_a_New_Root() { + DeepScanObjectAccessorToken(listOf("name")).read(readTree(FAMILY_JSON).toJsonNode()).isNewRoot shouldBe true + } + + @Test + fun should_handle_objects_on_different_levels() { + val json = readTree("""[{"a":1},{"a":2,"b":3},{"a":4,"b":5,"c":{"a":6,"b":7,"c":8}}]""") + DeepScanObjectAccessorToken(listOf("a")).read(json.toJsonNode()).element.toString() shouldBe """[1,2,4,6]""" + DeepScanObjectAccessorToken(listOf("c")).read(json.toJsonNode()).element.toString() shouldBe """[{"a":6,"b":7,"c":8},8]""" + DeepScanObjectAccessorToken( + listOf( + "a", + "c" + ) + ).read(json.toJsonNode()).element.toString() shouldBe """[1,2,4,{"a":6,"b":7,"c":8},6,8]""" + DeepScanObjectAccessorToken(listOf("a")).read(WildcardToken().read(json.toJsonNode())).element.toString() shouldBe """[1,2,4,6]""" + DeepScanObjectAccessorToken(listOf("c")).read(WildcardToken().read(json.toJsonNode())).element.toString() shouldBe """[{"a":6,"b":7,"c":8},8]""" + DeepScanObjectAccessorToken( + listOf( + "a", + "c" + ) + ).read(WildcardToken().read(json.toJsonNode())).element.toString() shouldBe """[1,2,4,{"a":6,"b":7,"c":8},6,8]""" + } +} diff --git a/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/tokens/DeepScanWildcardTokenTest.kt b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/tokens/DeepScanWildcardTokenTest.kt new file mode 100644 index 0000000..780c70c --- /dev/null +++ b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/tokens/DeepScanWildcardTokenTest.kt @@ -0,0 +1,15 @@ +package com.nfeld.jsonpathkt.tokens + +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import kotlin.test.Test + +class DeepScanWildcardTokenTest { + @Test + fun should_override_toString_hashCode_and_equals() { + DeepScanWildcardToken().toString() shouldBe "DeepScanWildcardToken" + DeepScanWildcardToken().hashCode() shouldBe "DeepScanWildcardToken".hashCode() + DeepScanWildcardToken() shouldBe DeepScanWildcardToken() + DeepScanWildcardToken() shouldNotBe ArrayAccessorToken(0) + } +} diff --git a/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/tokens/MultiArrayAccessorTokenTest.kt b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/tokens/MultiArrayAccessorTokenTest.kt new file mode 100644 index 0000000..c82b885 --- /dev/null +++ b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/tokens/MultiArrayAccessorTokenTest.kt @@ -0,0 +1,102 @@ +package com.nfeld.jsonpathkt.tokens + +import com.nfeld.jsonpathkt.emptyJsonObject +import com.nfeld.jsonpathkt.readTree +import com.nfeld.jsonpathkt.toJsonNode +import io.kotest.matchers.shouldBe +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonArray +import kotlin.test.Test +import kotlin.test.assertEquals + +class MultiArrayAccessorTokenTest { + @Test + fun should_get_items_at_specified_indices() { + MultiArrayAccessorToken( + listOf( + 0, + 1 + ) + ).read(emptyJsonObject().toJsonNode()).element.toString() shouldBe "[]" + + val expected = buildJsonArray { + add(JsonPrimitive(1)) + add(JsonPrimitive(3)) + } + assertEquals( + expected.toString(), + MultiArrayAccessorToken(listOf(0, -1)) + .read( + buildJsonArray { + add(JsonPrimitive(1)) + add(JsonPrimitive(2)) + add(JsonPrimitive(3)) + }.toJsonNode() + ) + .element + .toString() + ) + } + + @Test + fun should_get_specified_items_of_sublists_if_node_is_a_New_Root() { + val json = readTree("[1,[2],[3,4],[5,6,7]]") as JsonArray + MultiArrayAccessorToken( + listOf( + 0, + 1 + ) + ).read(json.toJsonNode(isNewRoot = true)).element.toString() shouldBe "[2,3,4,5,6]" + MultiArrayAccessorToken( + listOf( + 0, + -1 + ) + ).read(json.toJsonNode(isNewRoot = true)).element.toString() shouldBe "[2,2,3,4,5,7]" + } + + @Test + fun should_be_able_to_get_same_index_multiple_times() { + val json = readTree("[1,[2],[3,4],[5,6,7]]") as JsonArray + MultiArrayAccessorToken( + listOf( + 0, + 0, + 0 + ) + ).read(json.toJsonNode()).element.toString() shouldBe "[1,1,1]" + MultiArrayAccessorToken( + listOf( + 2, + 2 + ) + ).read(json.toJsonNode()).element.toString() shouldBe "[[3,4],[3,4]]" + MultiArrayAccessorToken( + listOf( + 0, + 0 + ) + ).read(json.toJsonNode(isNewRoot = true)).element.toString() shouldBe "[2,2,3,3,5,5]" + } + + @Test + fun should_get_characters_of_a_String_at_specified_indices() { + MultiArrayAccessorToken( + listOf( + 1, + 4 + ) + ).read(readTree("\"hello\"").toJsonNode()).element.toString() shouldBe """["e","o"]""" + } + + @Test + fun should_get_specified_characters_of_every_String_in_a_root_level_array() { + MultiArrayAccessorToken( + listOf( + 0, + 1 + ) + ).read(WildcardToken().read(readTree("""["hello","world"]""").toJsonNode())).element.toString() shouldBe """["h","e","w","o"]""" + } +} diff --git a/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/tokens/MultiObjectAccessorTokenTest.kt b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/tokens/MultiObjectAccessorTokenTest.kt new file mode 100644 index 0000000..3256a90 --- /dev/null +++ b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/tokens/MultiObjectAccessorTokenTest.kt @@ -0,0 +1,88 @@ +package com.nfeld.jsonpathkt.tokens + +import com.nfeld.jsonpathkt.emptyJsonArray +import com.nfeld.jsonpathkt.readTree +import com.nfeld.jsonpathkt.toJsonNode +import io.kotest.matchers.shouldBe +import kotlinx.serialization.json.JsonPrimitive +import kotlin.test.Test + +class MultiObjectAccessorTokenTest { + @Test + fun should_return_empty_if_accessing_non_root_array_or_scalars() { + // object accessor on an array should consistently return list + MultiObjectAccessorToken( + listOf( + "a", + "b" + ) + ).read(emptyJsonArray().toJsonNode()).element.toString() shouldBe "[]" + MultiObjectAccessorToken( + listOf( + "a", + "b" + ) + ).read(JsonPrimitive("yo").toJsonNode()).element.toString() shouldBe "[]" + MultiObjectAccessorToken( + listOf( + "a", + "b" + ) + ).read(JsonPrimitive(true).toJsonNode()).element.toString() shouldBe "[]" + } + + @Test + fun should_get_the_values_if_they_exist_in_object() { + MultiObjectAccessorToken( + listOf( + "a", + "b" + ) + ).read(readTree("""{"a":1,"b":2,"c":3}""").toJsonNode()).element.toString() shouldBe "[1,2]" + MultiObjectAccessorToken( + listOf( + "a", + "b" + ) + ).read(readTree("""{"a":1,"b":2}""").toJsonNode()).element.toString() shouldBe "[1,2]" + MultiObjectAccessorToken( + listOf( + "a", + "b" + ) + ).read(readTree("""{"a":1}""").toJsonNode()).element.toString() shouldBe "[1]" + } + + @Test + fun should_get_the_values_from_subcontainers_in_root_array_node() { + MultiObjectAccessorToken(listOf("a", "b")).read( + readTree("""[{"a":1,"b":2,"c":3}]""").toJsonNode( + isNewRoot = true + ) + ).element.toString() shouldBe "[1,2]" + MultiObjectAccessorToken( + listOf( + "a", + "b" + ) + ).read(readTree("""[{"a":1,"b":2}]""").toJsonNode(isNewRoot = true)).element.toString() shouldBe "[1,2]" + MultiObjectAccessorToken( + listOf( + "a", + "b" + ) + ).read(readTree("""[{"a":1}]""").toJsonNode(isNewRoot = true)).element.toString() shouldBe "[1]" + MultiObjectAccessorToken( + listOf( + "a", + "b" + ) + ).read(readTree("""[{}]""").toJsonNode(isNewRoot = true)).element.toString() shouldBe "[]" + MultiObjectAccessorToken( + listOf( + "a", + "b" + ) + ).read(readTree("""[]""").toJsonNode(isNewRoot = true)).element.toString() shouldBe "[]" + } +} diff --git a/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/tokens/ObjectAccessorTokenTest.kt b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/tokens/ObjectAccessorTokenTest.kt new file mode 100644 index 0000000..0bfbd19 --- /dev/null +++ b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/tokens/ObjectAccessorTokenTest.kt @@ -0,0 +1,32 @@ +package com.nfeld.jsonpathkt.tokens + +import com.nfeld.jsonpathkt.readTree +import com.nfeld.jsonpathkt.toJsonNode +import io.kotest.matchers.shouldBe +import kotlin.test.Test + +class ObjectAccessorTokenTest { + private val objJson = readTree("""{"key":1}""") + private val arrJson = readTree("""[{"key":1}]""") + + @Test + fun should_get_value_from_key_if_it_exists() { + ObjectAccessorToken("key").read(objJson.toJsonNode())?.element.toString() shouldBe "1" + } + + @Test + fun should_be_null_if_key_does_not_exist() { + ObjectAccessorToken("missing").read(objJson.toJsonNode()) shouldBe null + } + + @Test + fun should_be_null_if_node_is_an_ArrayNode() { + ObjectAccessorToken("key").read(arrJson.toJsonNode()) shouldBe null + } + + @Test + fun should_get_value_from_key_if_node_is_a_New_Root() { + val rootJson = WildcardToken().read(arrJson.toJsonNode()) // should not be null + ObjectAccessorToken("key").read(rootJson)?.element.toString() shouldBe "[1]" // list since it was root level + } +} diff --git a/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/tokens/WildcardTokenTest.kt b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/tokens/WildcardTokenTest.kt new file mode 100644 index 0000000..08452bf --- /dev/null +++ b/jsonpath/src/commonTest/kotlin/com/nfeld/jsonpathkt/tokens/WildcardTokenTest.kt @@ -0,0 +1,47 @@ +package com.nfeld.jsonpathkt.tokens + +import com.nfeld.jsonpathkt.emptyJsonArray +import com.nfeld.jsonpathkt.emptyJsonObject +import com.nfeld.jsonpathkt.readTree +import com.nfeld.jsonpathkt.toJsonNode +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import kotlin.test.Test + +class WildcardTokenTest { + @Test + fun should_handle_empty_cases() { + WildcardToken().read(emptyJsonArray().toJsonNode()).element.toString() shouldBe """[]""" + WildcardToken().read(emptyJsonObject().toJsonNode()).element.toString() shouldBe """[]""" + } + + @Test + fun should_get_values_from_objects_and_strip() { + val jsonObject = + readTree("""{ "some": "string", "int": 42, "object": { "key": "value" }, "array": [0, 1] }""") + WildcardToken().read(jsonObject.toJsonNode()).element.toString() shouldBe """["string",42,{"key":"value"},[0,1]]""" + } + + @Test + fun should_return_a_New_Root_if_root_list_replaced_with_another_list_before_modifying_values() { + val jsonArray = readTree("""["string", 42, { "key": "value" }, [0, 1] ]""") + WildcardToken().read(jsonArray.toJsonNode()).element.toString() shouldBe """["string",42,{"key":"value"},[0,1]]""" + } + + @Test + fun should_drop_scalars_and_move_everything_down_on_root_level_array() { + val jsonArray = readTree("""["string", 42, { "key": "value" }, [0, 1] ]""") + val res1 = WildcardToken().read(jsonArray.toJsonNode()) + res1.isNewRoot shouldBe true + val res2 = WildcardToken().read(res1) + res2.element.toString() shouldBe """["value",0,1]""" + } + + @Test + fun should_override_toString_hashCode_and_equals() { + WildcardToken().toString() shouldBe "WildcardToken" + WildcardToken().hashCode() shouldBe "WildcardToken".hashCode() + WildcardToken() shouldBe WildcardToken() + WildcardToken() shouldNotBe ArrayAccessorToken(0) + } +} diff --git a/jsonpath/src/jvmBenchmark/kotlin/com/nfeld/jsonpathkt/BenchmarkTest.kt b/jsonpath/src/jvmBenchmark/kotlin/com/nfeld/jsonpathkt/BenchmarkTest.kt new file mode 100644 index 0000000..6258804 --- /dev/null +++ b/jsonpath/src/jvmBenchmark/kotlin/com/nfeld/jsonpathkt/BenchmarkTest.kt @@ -0,0 +1,181 @@ +package com.nfeld.jsonpathkt + +import com.jayway.jsonpath.Configuration +import com.jayway.jsonpath.spi.json.JacksonJsonProvider +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import com.jayway.jsonpath.JsonPath as JaywayJsonPath +import com.jayway.jsonpath.spi.cache.CacheProvider as JaywayCacheProvider +import com.jayway.jsonpath.spi.cache.NOOPCache as JaywayNOOPCache + +private const val DEFAULT_RUNS = 30 +private const val DEFAULT_CALLS_PER_RUN = 80000 +private var printReadmeFormat = false +private val timestamp: Long + get() = System.currentTimeMillis() + +private fun benchmark( + callsPerRun: Int = DEFAULT_CALLS_PER_RUN, + runs: Int = DEFAULT_RUNS, + f: () -> Unit +): Long { + // warmup + repeat(3) { + f() + } + + val times = mutableListOf() + + for (i in 0 until runs) { + val t1 = timestamp + for (k in 0 until callsPerRun) { + f() + } + val t2 = timestamp + times.add(t2 - t1) + } + + return times.average().toLong() +} + +private fun benchmarkJsonPathKt( + path: String, + callsPerRun: Int = DEFAULT_CALLS_PER_RUN, + runs: Int = DEFAULT_RUNS +): Long { + val json = JsonPath.parse(LARGE_JSON)!! // pre-parse json + return benchmark(callsPerRun, runs) { JsonPath(path).read(json) } +} + +private fun benchmarkJaywayJsonPath( + path: String, + callsPerRun: Int = DEFAULT_CALLS_PER_RUN, + runs: Int = DEFAULT_RUNS +): Long { + val jaywayConfig = Configuration.defaultConfiguration().jsonProvider(JacksonJsonProvider()) + val documentContext = JaywayJsonPath.parse(LARGE_JSON, jaywayConfig) + return benchmark(callsPerRun, runs) { documentContext.read(path) } +} + +private fun runBenchmarksAndPrintResults( + path: String, + callsPerRun: Int = DEFAULT_CALLS_PER_RUN, + runs: Int = DEFAULT_RUNS +) { + val kt = benchmarkJsonPathKt(path, callsPerRun, runs) + val jayway = benchmarkJaywayJsonPath(path, callsPerRun, runs) + + if (printReadmeFormat) { + println("| $path | $kt ms | $jayway ms |") + } else { + println("$path kt: ${kt}, jsonpath: $jayway") + } +} + +class BenchmarkTest { + companion object { + @BeforeAll + @JvmStatic + fun before() { + println("Setting up BenchmarkTest") + + printReadmeFormat = System.getProperty("readmeFormat")?.toBoolean() ?: false + + JaywayCacheProvider.setCache(JaywayNOOPCache()) + } + } + + @Test + fun benchmarkDeepPath() { + runBenchmarksAndPrintResults("$[0].friends[1].other.a.b['c']") + } + + @Test + fun benchmarkShallowPath() { + runBenchmarksAndPrintResults("$[2]._id") + } + + @Test + fun benchmarkCompilingPath() { + + fun compile(path: String) { + val kt = benchmark { JsonPath(path) } + val jayway = benchmark { JaywayJsonPath.compile(path) } + + val numTokens = PathCompiler.compile(path).size + val name = "${path.length} chars, $numTokens tokens" + + if (printReadmeFormat) { + println("| $name | $kt ms | $jayway ms |") + } else { + println("$name kt: ${kt}, jsonpath: $jayway") + } + } + + compile("$.hello") + compile("$.hello.world[0]") + compile("$[0].friends[1].other.a.b['c']") + compile("$[0].friends[1].other.a.b['c'][5].niko[2].hello.world[6][9][0].id") + compile("$[0].friends[1]..other[2].a.b['c'][5].niko[2]..hello[0].world[6][9]..['a','b','c'][0].id") + } + + @Test + fun benchmarkDeepScans() { + val callsPerRun = 20000 + val runs = 10 + runBenchmarksAndPrintResults("$..name", callsPerRun, runs) + runBenchmarksAndPrintResults("$..['email','name']", callsPerRun, runs) + runBenchmarksAndPrintResults("$..[1]", callsPerRun, runs) + } + + @Test + fun benchmarkDeepScanRanges() { + val callsPerRun = 20000 + val runs = 10 + runBenchmarksAndPrintResults("$..[:2]", callsPerRun, runs) + runBenchmarksAndPrintResults("$..[2:]", callsPerRun, runs) + + // jayway jsonpath gives empty response for this so not valid comparison + // runBenchmarksAndPrintResults("$..[1:-1]", callsPerRun, runs) + } + + @Test + fun benchmarkArrayAccessFromEndElement() { + runBenchmarksAndPrintResults("$[0]['tags'][-3]") + } + + @Test + fun benchmarkArrayRangeFromStart() { + runBenchmarksAndPrintResults("$[0]['tags'][:3]") + } + + @Test + fun benchmarkArrayRangeToEndElement() { + runBenchmarksAndPrintResults("$[0]['tags'][3:]") + } + + @Test + fun benchmarkArrayRange() { + runBenchmarksAndPrintResults("$[0]['tags'][3:5]") + } + + @Test + fun benchmarkMultiArrayAccess() { + runBenchmarksAndPrintResults("$[0]['tags'][0,3,5]") + } + + @Test + fun benchmarkMultiObjectAccess() { + runBenchmarksAndPrintResults("$[0]['latitude','longitude','isActive']") + } + + @Test + fun benchmarkWildcard() { + runBenchmarksAndPrintResults("$[0]['tags'].*") + } + + @Test + fun benchmarkRecursiveWildcard() { + runBenchmarksAndPrintResults("$[0]..*") + } +} diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock new file mode 100644 index 0000000..d870ecd --- /dev/null +++ b/kotlin-js-store/yarn.lock @@ -0,0 +1,1947 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@colors/colors@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" + integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== + +"@discoveryjs/json-ext@^0.5.0": + version "0.5.7" + resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" + integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== + +"@jridgewell/gen-mapping@^0.3.0": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" + integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" + integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== + +"@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/source-map@^0.3.3": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.5.tgz#a3bb4d5c6825aab0d281268f47f6ad5853431e91" + integrity sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.14" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + +"@jridgewell/sourcemap-codec@^1.4.14": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@jridgewell/trace-mapping@^0.3.17": + version "0.3.19" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz#f8a3249862f91be48d3127c3cfe992f79b4b8811" + integrity sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@jridgewell/trace-mapping@^0.3.9": + version "0.3.17" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985" + integrity sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g== + dependencies: + "@jridgewell/resolve-uri" "3.1.0" + "@jridgewell/sourcemap-codec" "1.4.14" + +"@socket.io/component-emitter@~3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553" + integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg== + +"@types/cookie@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" + integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== + +"@types/cors@^2.8.12": + version "2.8.12" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080" + integrity sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw== + +"@types/eslint-scope@^3.7.3": + version "3.7.4" + resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16" + integrity sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA== + dependencies: + "@types/eslint" "*" + "@types/estree" "*" + +"@types/eslint@*": + version "8.4.9" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.4.9.tgz#f7371980148697f4b582b086630319b55324b5aa" + integrity sha512-jFCSo4wJzlHQLCpceUhUnXdrPuCNOjGFMQ8Eg6JXxlz3QaCKOb7eGi2cephQdM4XTYsNej69P9JDJ1zqNIbncQ== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree@*": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2" + integrity sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ== + +"@types/estree@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194" + integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA== + +"@types/json-schema@*", "@types/json-schema@^7.0.8": + version "7.0.11" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" + integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== + +"@types/node@*", "@types/node@>=10.0.0": + version "18.11.9" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.9.tgz#02d013de7058cea16d36168ef2fc653464cfbad4" + integrity sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg== + +"@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24" + integrity sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q== + dependencies: + "@webassemblyjs/helper-numbers" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + +"@webassemblyjs/floating-point-hex-parser@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz#dacbcb95aff135c8260f77fa3b4c5fea600a6431" + integrity sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw== + +"@webassemblyjs/helper-api-error@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" + integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== + +"@webassemblyjs/helper-buffer@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz#b66d73c43e296fd5e88006f18524feb0f2c7c093" + integrity sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA== + +"@webassemblyjs/helper-numbers@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz#cbce5e7e0c1bd32cf4905ae444ef64cea919f1b5" + integrity sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g== + dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.11.6" + "@webassemblyjs/helper-api-error" "1.11.6" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/helper-wasm-bytecode@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" + integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== + +"@webassemblyjs/helper-wasm-section@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz#ff97f3863c55ee7f580fd5c41a381e9def4aa577" + integrity sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/wasm-gen" "1.11.6" + +"@webassemblyjs/ieee754@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz#bb665c91d0b14fffceb0e38298c329af043c6e3a" + integrity sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz#70e60e5e82f9ac81118bc25381a0b283893240d7" + integrity sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" + integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== + +"@webassemblyjs/wasm-edit@^1.11.5": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz#c72fa8220524c9b416249f3d94c2958dfe70ceab" + integrity sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/helper-wasm-section" "1.11.6" + "@webassemblyjs/wasm-gen" "1.11.6" + "@webassemblyjs/wasm-opt" "1.11.6" + "@webassemblyjs/wasm-parser" "1.11.6" + "@webassemblyjs/wast-printer" "1.11.6" + +"@webassemblyjs/wasm-gen@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz#fb5283e0e8b4551cc4e9c3c0d7184a65faf7c268" + integrity sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wasm-opt@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz#d9a22d651248422ca498b09aa3232a81041487c2" + integrity sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/wasm-gen" "1.11.6" + "@webassemblyjs/wasm-parser" "1.11.6" + +"@webassemblyjs/wasm-parser@1.11.6", "@webassemblyjs/wasm-parser@^1.11.5": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz#bb85378c527df824004812bbdb784eea539174a1" + integrity sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-api-error" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wast-printer@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz#a7bf8dd7e362aeb1668ff43f35cb849f188eff20" + integrity sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@xtuc/long" "4.2.2" + +"@webpack-cli/configtest@^2.1.0": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-2.1.1.tgz#3b2f852e91dac6e3b85fb2a314fb8bef46d94646" + integrity sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw== + +"@webpack-cli/info@^2.0.1": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-2.0.2.tgz#cc3fbf22efeb88ff62310cf885c5b09f44ae0fdd" + integrity sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A== + +"@webpack-cli/serve@^2.0.3": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.5.tgz#325db42395cd49fe6c14057f9a900e427df8810e" + integrity sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ== + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +abab@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" + integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== + +accepts@~1.3.4: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn-import-assertions@^1.7.6: + version "1.8.0" + resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz#ba2b5939ce62c238db6d93d81c9b111b29b855e9" + integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw== + +acorn@^8.7.1, acorn@^8.8.2: + version "8.10.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" + integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== + +ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv@^6.12.5: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-colors@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +anymatch@~3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" + integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base64id@2.0.0, base64id@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" + integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog== + +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + +body-parser@^1.19.0: + version "1.20.1" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" + integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== + dependencies: + bytes "3.1.2" + content-type "~1.0.4" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.1" + type-is "~1.6.18" + unpipe "1.0.0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +braces@^3.0.2, braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + +browserslist@^4.14.5: + version "4.21.4" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.4.tgz#e7496bbc67b9e39dd0f98565feccdcb0d4ff6987" + integrity sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw== + dependencies: + caniuse-lite "^1.0.30001400" + electron-to-chromium "^1.4.251" + node-releases "^2.0.6" + update-browserslist-db "^1.0.9" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + +camelcase@^6.0.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +caniuse-lite@^1.0.30001400: + version "1.0.30001429" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001429.tgz#70cdae959096756a85713b36dd9cb82e62325639" + integrity sha512-511ThLu1hF+5RRRt0zYCf2U2yRr9GPF6m5y90SBCWsvSoYoW7yAGlv/elyPaNfvGCkp6kj/KFZWU0BMA69Prsg== + +chalk@^4.1.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chokidar@3.5.3, chokidar@^3.5.1: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chrome-trace-event@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" + integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" + integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== + dependencies: + is-plain-object "^2.0.4" + kind-of "^6.0.2" + shallow-clone "^3.0.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +colorette@^2.0.14: + version "2.0.19" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" + integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== + +commander@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" + integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +connect@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/connect/-/connect-3.7.0.tgz#5d49348910caa5e07a01800b030d0c35f20484f8" + integrity sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ== + dependencies: + debug "2.6.9" + finalhandler "1.1.2" + parseurl "~1.3.3" + utils-merge "1.0.1" + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + +cookie@~0.4.1: + version "0.4.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" + integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== + +cors@~2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + +cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +custom-event@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425" + integrity sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg== + +date-format@^4.0.14: + version "4.0.14" + resolved "https://registry.yarnpkg.com/date-format/-/date-format-4.0.14.tgz#7a8e584434fb169a521c8b7aa481f355810d9400" + integrity sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg== + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@4.3.4, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +decamelize@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" + integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +di@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c" + integrity sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA== + +diff@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" + integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== + +dom-serialize@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b" + integrity sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ== + dependencies: + custom-event "~1.0.0" + ent "~2.2.0" + extend "^3.0.0" + void-elements "^2.0.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +electron-to-chromium@^1.4.251: + version "1.4.284" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz#61046d1e4cab3a25238f6bf7413795270f125592" + integrity sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +engine.io-parser@~5.0.3: + version "5.0.4" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.4.tgz#0b13f704fa9271b3ec4f33112410d8f3f41d0fc0" + integrity sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg== + +engine.io@~6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.2.0.tgz#003bec48f6815926f2b1b17873e576acd54f41d0" + integrity sha512-4KzwW3F3bk+KlzSOY57fj/Jx6LyRQ1nbcyIadehl+AnXjKT7gDO0ORdRi/84ixvMKTym6ZKuxvbzN62HDDU1Lg== + dependencies: + "@types/cookie" "^0.4.1" + "@types/cors" "^2.8.12" + "@types/node" ">=10.0.0" + accepts "~1.3.4" + base64id "2.0.0" + cookie "~0.4.1" + cors "~2.8.5" + debug "~4.3.1" + engine.io-parser "~5.0.3" + ws "~8.2.3" + +enhanced-resolve@^5.13.0: + version "5.15.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35" + integrity sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +ent@~2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d" + integrity sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA== + +envinfo@^7.7.3: + version "7.8.1" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475" + integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw== + +es-module-lexer@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.3.0.tgz#6be9c9e0b4543a60cd166ff6f8b4e9dae0b0c16f" + integrity sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA== + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-scope@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +eventemitter3@^4.0.0: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + +events@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +extend@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fastest-levenshtein@^1.0.12: + version "1.0.16" + resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" + integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + +find-up@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +find-up@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + +flatted@^3.2.7: + version "3.2.7" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" + integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== + +follow-redirects@^1.0.0: + version "1.15.2" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" + integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== + +format-util@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/format-util/-/format-util-1.0.5.tgz#1ffb450c8a03e7bccffe40643180918cc297d271" + integrity sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg== + +fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.0.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.3.tgz#063c84329ad93e83893c7f4f243ef63ffa351385" + integrity sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.3" + +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +glob@7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.1.3, glob@^7.1.7: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + +graceful-fs@^4.2.10: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +he@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-proxy@^1.18.1: + version "1.18.1" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" + integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== + dependencies: + eventemitter3 "^4.0.0" + follow-redirects "^1.0.0" + requires-port "^1.0.0" + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +iconv-lite@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +import-local@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" + integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +interpret@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-3.1.1.tgz#5be0ceed67ca79c6c4bc5cf0d7ee843dcea110c4" + integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-core-module@^2.13.0: + version "2.13.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.0.tgz#bb52aa6e2cbd49a30c2ba68c42bf3435ba6072db" + integrity sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ== + dependencies: + has "^1.0.3" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-plain-obj@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + +is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + +isbinaryfile@^4.0.8: + version "4.0.10" + resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.10.tgz#0c5b5e30c2557a2f06febd37b7322946aaee42b3" + integrity sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== + +jest-worker@^27.4.5: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" + integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +js-yaml@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +json-parse-even-better-errors@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg== + optionalDependencies: + graceful-fs "^4.1.6" + +karma-chrome-launcher@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz#eb9c95024f2d6dfbb3748d3415ac9b381906b9a9" + integrity sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q== + dependencies: + which "^1.2.1" + +karma-mocha@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/karma-mocha/-/karma-mocha-2.0.1.tgz#4b0254a18dfee71bdbe6188d9a6861bf86b0cd7d" + integrity sha512-Tzd5HBjm8his2OA4bouAsATYEpZrp9vC7z5E5j4C5Of5Rrs1jY67RAwXNcVmd/Bnk1wgvQRou0zGVLey44G4tQ== + dependencies: + minimist "^1.2.3" + +karma-sourcemap-loader@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/karma-sourcemap-loader/-/karma-sourcemap-loader-0.4.0.tgz#b01d73f8f688f533bcc8f5d273d43458e13b5488" + integrity sha512-xCRL3/pmhAYF3I6qOrcn0uhbQevitc2DERMPH82FMnG+4WReoGcGFZb1pURf2a5apyrOHRdvD+O6K7NljqKHyA== + dependencies: + graceful-fs "^4.2.10" + +karma-webpack@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/karma-webpack/-/karma-webpack-5.0.0.tgz#2a2c7b80163fe7ffd1010f83f5507f95ef39f840" + integrity sha512-+54i/cd3/piZuP3dr54+NcFeKOPnys5QeM1IY+0SPASwrtHsliXUiCL50iW+K9WWA7RvamC4macvvQ86l3KtaA== + dependencies: + glob "^7.1.3" + minimatch "^3.0.4" + webpack-merge "^4.1.5" + +karma@6.4.2: + version "6.4.2" + resolved "https://registry.yarnpkg.com/karma/-/karma-6.4.2.tgz#a983f874cee6f35990c4b2dcc3d274653714de8e" + integrity sha512-C6SU/53LB31BEgRg+omznBEMY4SjHU3ricV6zBcAe1EeILKkeScr+fZXtaI5WyDbkVowJxxAI6h73NcFPmXolQ== + dependencies: + "@colors/colors" "1.5.0" + body-parser "^1.19.0" + braces "^3.0.2" + chokidar "^3.5.1" + connect "^3.7.0" + di "^0.0.1" + dom-serialize "^2.2.1" + glob "^7.1.7" + graceful-fs "^4.2.6" + http-proxy "^1.18.1" + isbinaryfile "^4.0.8" + lodash "^4.17.21" + log4js "^6.4.1" + mime "^2.5.2" + minimatch "^3.0.4" + mkdirp "^0.5.5" + qjobs "^1.2.0" + range-parser "^1.2.1" + rimraf "^3.0.2" + socket.io "^4.4.1" + source-map "^0.6.1" + tmp "^0.2.1" + ua-parser-js "^0.7.30" + yargs "^16.1.1" + +kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +loader-runner@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" + integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash@^4.17.15, lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +log-symbols@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +log4js@^6.4.1: + version "6.7.0" + resolved "https://registry.yarnpkg.com/log4js/-/log4js-6.7.0.tgz#fff671a74b2f6e956d135c3c756c79072809a23b" + integrity sha512-KA0W9ffgNBLDj6fZCq/lRbgR6ABAodRIDHrZnS48vOtfKa4PzWImb0Md1lmGCdO3n3sbCm/n1/WmrNlZ8kCI3Q== + dependencies: + date-format "^4.0.14" + debug "^4.3.4" + flatted "^3.2.7" + rfdc "^1.3.0" + streamroller "^3.1.3" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.27, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@^2.5.2: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== + +minimatch@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b" + integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^3.0.4, minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.2.3, minimist@^1.2.6: + version "1.2.7" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" + integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== + +mkdirp@^0.5.5: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +mocha@10.2.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.2.0.tgz#1fd4a7c32ba5ac372e03a17eef435bd00e5c68b8" + integrity sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg== + dependencies: + ansi-colors "4.1.1" + browser-stdout "1.3.1" + chokidar "3.5.3" + debug "4.3.4" + diff "5.0.0" + escape-string-regexp "4.0.0" + find-up "5.0.0" + glob "7.2.0" + he "1.2.0" + js-yaml "4.1.0" + log-symbols "4.1.0" + minimatch "5.0.1" + ms "2.1.3" + nanoid "3.3.3" + serialize-javascript "6.0.0" + strip-json-comments "3.1.1" + supports-color "8.1.1" + workerpool "6.2.1" + yargs "16.2.0" + yargs-parser "20.2.4" + yargs-unparser "2.0.0" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +nanoid@3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25" + integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +node-releases@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503" + integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +object-assign@^4: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-inspect@^1.9.0: + version "1.12.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" + integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww== + dependencies: + ee-first "1.1.1" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.0.4, picomatch@^2.2.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +punycode@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +qjobs@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071" + integrity sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg== + +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +range-parser@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" + integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +rechoir@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.8.0.tgz#49f866e0d32146142da3ad8f0eff352b3215ff22" + integrity sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ== + dependencies: + resolve "^1.20.0" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve@^1.20.0: + version "1.22.4" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.4.tgz#1dc40df46554cdaf8948a486a10f6ba1e2026c34" + integrity sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +rfdc@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" + integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== + +rimraf@^3.0.0, rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +safe-buffer@^5.1.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +schema-utils@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281" + integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + +schema-utils@^3.1.2: + version "3.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" + integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + +serialize-javascript@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" + integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== + dependencies: + randombytes "^2.1.0" + +serialize-javascript@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz#b206efb27c3da0b0ab6b52f48d170b7996458e5c" + integrity sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w== + dependencies: + randombytes "^2.1.0" + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + +socket.io-adapter@~2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.4.0.tgz#b50a4a9ecdd00c34d4c8c808224daa1a786152a6" + integrity sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg== + +socket.io-parser@~4.2.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.1.tgz#01c96efa11ded938dcb21cbe590c26af5eff65e5" + integrity sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + +socket.io@^4.4.1: + version "4.5.3" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.5.3.tgz#44dffea48d7f5aa41df4a66377c386b953bc521c" + integrity sha512-zdpnnKU+H6mOp7nYRXH4GNv1ux6HL6+lHL8g7Ds7Lj8CkdK1jJK/dlwsKDculbyOHifcJ0Pr/yeXnZQ5GeFrcg== + dependencies: + accepts "~1.3.4" + base64id "~2.0.0" + debug "~4.3.2" + engine.io "~6.2.0" + socket.io-adapter "~2.4.0" + socket.io-parser "~4.2.0" + +source-map-js@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== + +source-map-loader@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-4.0.1.tgz#72f00d05f5d1f90f80974eda781cbd7107c125f2" + integrity sha512-oqXpzDIByKONVY8g1NUPOTQhe0UTU5bWUl32GSkqK2LjJj0HmwTMVKxcUip0RgAYhY1mqgOxjbQM48a0mmeNfA== + dependencies: + abab "^2.0.6" + iconv-lite "^0.6.3" + source-map-js "^1.0.2" + +source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0, source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +statuses@~1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== + +streamroller@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-3.1.3.tgz#d95689a8c29b30d093525d0baffe6616fd62ca7e" + integrity sha512-CphIJyFx2SALGHeINanjFRKQ4l7x2c+rXYJ4BMq0gd+ZK0gi4VT8b+eHe2wi58x4UayBAKx4xtHpXT/ea1cz8w== + dependencies: + date-format "^4.0.14" + debug "^4.3.4" + fs-extra "^8.1.0" + +string-width@^4.1.0, string-width@^4.2.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-json-comments@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +supports-color@8.1.1, supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +tapable@^2.1.1, tapable@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + +terser-webpack-plugin@^5.3.7: + version "5.3.9" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz#832536999c51b46d468067f9e37662a3b96adfe1" + integrity sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA== + dependencies: + "@jridgewell/trace-mapping" "^0.3.17" + jest-worker "^27.4.5" + schema-utils "^3.1.1" + serialize-javascript "^6.0.1" + terser "^5.16.8" + +terser@^5.16.8: + version "5.19.4" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.19.4.tgz#941426fa482bf9b40a0308ab2b3cd0cf7c775ebd" + integrity sha512-6p1DjHeuluwxDXcuT9VR8p64klWJKo1ILiy19s6C9+0Bh2+NWTX6nD9EPppiER4ICkHDVB1RkVpin/YW2nQn/g== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.8.2" + commander "^2.20.0" + source-map-support "~0.5.20" + +tmp@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" + integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== + dependencies: + rimraf "^3.0.0" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typescript@5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.4.tgz#b217fd20119bd61a94d4011274e0ab369058da3b" + integrity sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw== + +ua-parser-js@^0.7.30: + version "0.7.32" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.32.tgz#cd8c639cdca949e30fa68c44b7813ef13e36d211" + integrity sha512-f9BESNVhzlhEFf2CHMSj40NWOjYPl1YKYbrvIr/hFTDEmLq7SRbWvm7FcdcpCYT95zrOhC7gZSxjdnnTpBcwVw== + +universalify@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +update-browserslist-db@^1.0.9: + version "1.0.10" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3" + integrity sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +vary@^1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +void-elements@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" + integrity sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung== + +watchpack@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" + integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + +webpack-cli@5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-5.1.0.tgz#abc4b1f44b50250f2632d8b8b536cfe2f6257891" + integrity sha512-a7KRJnCxejFoDpYTOwzm5o21ZXMaNqtRlvS183XzGDUPRdVEzJNImcQokqYZ8BNTnk9DkKiuWxw75+DCCoZ26w== + dependencies: + "@discoveryjs/json-ext" "^0.5.0" + "@webpack-cli/configtest" "^2.1.0" + "@webpack-cli/info" "^2.0.1" + "@webpack-cli/serve" "^2.0.3" + colorette "^2.0.14" + commander "^10.0.1" + cross-spawn "^7.0.3" + envinfo "^7.7.3" + fastest-levenshtein "^1.0.12" + import-local "^3.0.2" + interpret "^3.1.1" + rechoir "^0.8.0" + webpack-merge "^5.7.3" + +webpack-merge@^4.1.5: + version "4.2.2" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.2.2.tgz#a27c52ea783d1398afd2087f547d7b9d2f43634d" + integrity sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g== + dependencies: + lodash "^4.17.15" + +webpack-merge@^5.7.3: + version "5.8.0" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.8.0.tgz#2b39dbf22af87776ad744c390223731d30a68f61" + integrity sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q== + dependencies: + clone-deep "^4.0.1" + wildcard "^2.0.0" + +webpack-sources@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" + integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== + +webpack@5.82.0: + version "5.82.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.82.0.tgz#3c0d074dec79401db026b4ba0fb23d6333f88e7d" + integrity sha512-iGNA2fHhnDcV1bONdUu554eZx+XeldsaeQ8T67H6KKHl2nUSwX8Zm7cmzOA46ox/X1ARxf7Bjv8wQ/HsB5fxBg== + dependencies: + "@types/eslint-scope" "^3.7.3" + "@types/estree" "^1.0.0" + "@webassemblyjs/ast" "^1.11.5" + "@webassemblyjs/wasm-edit" "^1.11.5" + "@webassemblyjs/wasm-parser" "^1.11.5" + acorn "^8.7.1" + acorn-import-assertions "^1.7.6" + browserslist "^4.14.5" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.13.0" + es-module-lexer "^1.2.1" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.9" + json-parse-even-better-errors "^2.3.1" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^3.1.2" + tapable "^2.1.1" + terser-webpack-plugin "^5.3.7" + watchpack "^2.4.0" + webpack-sources "^3.2.3" + +which@^1.2.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wildcard@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec" + integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw== + +workerpool@6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" + integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +ws@~8.2.3: + version "8.2.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba" + integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yargs-parser@20.2.4: + version "20.2.4" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" + integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== + +yargs-parser@^20.2.2: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs-unparser@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" + integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== + dependencies: + camelcase "^6.0.0" + decamelize "^4.0.0" + flat "^5.0.2" + is-plain-obj "^2.1.0" + +yargs@16.2.0, yargs@^16.1.1: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..0ae2ce5 --- /dev/null +++ b/renovate.json @@ -0,0 +1,19 @@ +{ + "extends": [ + "config:base" + ], + "enabledManagers": ["gradle", "gradle-wrapper", "github-actions"], + "labels": ["dependencies"], + "prHourlyLimit": 3, + "packageRules": [ + { + "matchDatasources": ["maven"], + "depType": "dependencies", + "registryUrls": [ + "https://repo.maven.apache.org/maven2/", + "https://dl.google.com/dl/android/maven2/", + "https://plugins.gradle.org/m2" + ] + } + ] +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 7e1ab7c..cb09de2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,18 +1,35 @@ +pluginManagement { + repositories { + mavenCentral() + + gradlePluginPortal() + } +} + +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") + +dependencyResolutionManagement { + @Suppress("UnstableApiUsage") + repositories { + mavenCentral() + } +} + plugins { - id("com.gradle.enterprise") version "3.10.3" + id("com.eygraber.conventions.settings") version "0.0.49" + id("com.gradle.enterprise") version "3.14.1" } rootProject.name = "jsonpathkt" -val publishBuildScan = providers.gradleProperty("publishBuildScan") - gradleEnterprise { buildScan { termsOfServiceUrl = "https://gradle.com/terms-of-service" - termsOfServiceAgree = "yes" - - if (publishBuildScan.isPresent) { + if(System.getenv("CI") != null) { + termsOfServiceAgree = "yes" publishAlways() } } } + +include(":jsonpath") diff --git a/src/main/kotlin/com/nfeld/jsonpathkt/JsonPath.kt b/src/main/kotlin/com/nfeld/jsonpathkt/JsonPath.kt deleted file mode 100644 index 4b0dd81..0000000 --- a/src/main/kotlin/com/nfeld/jsonpathkt/JsonPath.kt +++ /dev/null @@ -1,102 +0,0 @@ -package com.nfeld.jsonpathkt - -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.module.kotlin.convertValue -import com.nfeld.jsonpathkt.cache.CacheProvider -import com.nfeld.jsonpathkt.util.JacksonUtil - -class JsonPath(path: String) { - - private val path: String - val tokens: List - - /** - * Trim given path string and compile it on initialization - */ - init { - this.path = path.trim() - - val cache = CacheProvider.getCache() - val cachedJsonPath = cache?.get(this.path) - if (cachedJsonPath != null) { - tokens = cachedJsonPath.tokens - } else { - tokens = PathCompiler.compile(this.path) - cache?.put(this.path, this) - } - } - - /** - * Read the value at path in given JSON string - * - * @return Given type if value in path exists, null otherwise - */ - inline fun readFromJson(jsonString: String): T? { - return parse(jsonString)?.let { readFromJson(it) } - } - - /** - * Read the value at path in given jackson JsonNode Object - * - * @return Given type if value in path exists, null otherwise - */ - inline fun readFromJson(json: JsonNode): T? { - if (json.isMissingNode || json.isNull) { - return null - } - - val lastValue = tokens.fold(initial = json) { valueAtPath: JsonNode?, nextToken: Token -> - valueAtPath?.let { nextToken.read(it) } - } - - return when { - lastValue == null || lastValue.isNull || lastValue.isMissingNode -> null - else -> { - try { - JacksonUtil.mapper.convertValue(lastValue) - } catch (e: Exception) { - null - } - } - } - } - - /** - * Check if a ArrayNode contains only primitive values (in this case, non-ObjectNode/ArrayNode). - */ -// private fun containsOnlyPrimitives(arrayNode: ArrayNode) : Boolean { -// arrayNode.forEach { -// if (it.isObject || it.isArray) { -// return false // fail fast -// } -// } -// return true -// } - -// private fun isSpecialChar(c: Char): Boolean { -// return c == '"' || c == '\\' || c == '/' || c == 'b' || c == 'f' || c == 'n' || c == 'r' || c == 't' -// } - - companion object { - /** - * Parse JSON string and return successful [JsonNode] or null otherwise - * - * @param jsonString JSON string to parse - * @return instance of parsed jackson [JsonNode] object, or null - */ - @JvmStatic - fun parse(jsonString: String?): JsonNode? { - return jsonString?.let { - try { - val parsed = JacksonUtil.mapper.readTree(jsonString) - if (!parsed.isMissingNode && !parsed.isNull) { - parsed - } else null - } catch (e: Exception) { - null - } - } - } - - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/nfeld/jsonpathkt/Token.kt b/src/main/kotlin/com/nfeld/jsonpathkt/Token.kt deleted file mode 100644 index ea9aeb8..0000000 --- a/src/main/kotlin/com/nfeld/jsonpathkt/Token.kt +++ /dev/null @@ -1,477 +0,0 @@ -package com.nfeld.jsonpathkt - -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.node.ArrayNode -import com.fasterxml.jackson.databind.node.ObjectNode -import com.fasterxml.jackson.databind.node.TextNode -import com.nfeld.jsonpathkt.extension.getValueIfNotNullOrMissing -import com.nfeld.jsonpathkt.extension.isNotNullOrMissing -import com.nfeld.jsonpathkt.util.RootLevelArrayNode - -/** - * Accesses value at [index] from [ArrayNode] - * - * @param index index to access, can be negative which means to access from end - */ -internal data class ArrayAccessorToken(val index: Int) : Token { - override fun read(json: JsonNode): JsonNode? { - return read(json, index) - } - - companion object { - fun read(json: JsonNode, index: Int): JsonNode? { - return when (json) { - is RootLevelArrayNode -> { - // iterate through all items on root and get items from all sublists - val result = RootLevelArrayNode() - json.forEach { node -> - read(node, index)?.let { - if (it.isNotNullOrMissing()) { - result.add(it) - } - } - } - result - } - is ArrayNode -> { - // get the value at index directly - readValueAtIndex(json, index) - } - is TextNode -> { - val str = json.asText() - if (index < 0) { - val indexFromLast = str.length + index - if (indexFromLast >= 0 && indexFromLast < str.length) { - return TextNode(str[indexFromLast].toString()) - } else null - } else if (index < str.length) { - TextNode(str[index].toString()) - } else null - } - else -> null - } - } - - private fun readValueAtIndex(arrayNode: ArrayNode, index: Int): JsonNode? { - if (index < 0) { - val indexFromLast = arrayNode.size() + index - if (indexFromLast >= 0) { - return arrayNode.getValueIfNotNullOrMissing(indexFromLast) - } - } - return arrayNode.getValueIfNotNullOrMissing(index) - } - } -} - -/** - * Accesses values at [indices] from [ArrayNode]. When read, value returned will be [ArrayNode] of values - * at requested indices in given order. - * - * @param indices indices to access, can be negative which means to access from end - */ -internal data class MultiArrayAccessorToken(val indices: List) : Token { - override fun read(json: JsonNode): JsonNode { - val result = RootLevelArrayNode() - when (json) { - is RootLevelArrayNode -> { - // needs to be flattened, thus we iterate for each subnode before passing the reading down - json.forEach { node -> - indices.forEach { index -> - ArrayAccessorToken.read(node, index)?.let { - if (it.isNotNullOrMissing()) { - result.add(it) - } - } - } - } - } - else -> { - indices.forEach { index -> - ArrayAccessorToken.read(json, index)?.let { - if (it.isNotNullOrMissing()) { - result.add(it) - } - } - } - } - } - return result - } -} - -/** - * Accesses values from [ArrayNode] in range from [startIndex] to either [endIndex] or [offsetFromEnd] from end. - * When read, value returned will be ArrayNode of values at requested indices in order of values in range. - * - * @param startIndex starting index of range, inclusive. Can be negative. - * @param endIndex ending index of range, exclusive. Null if using [offsetFromEnd]. Can be positive only - * @param offsetFromEnd offset of values from end of array. 0 if using [endIndex]. Can be negative only - */ -internal data class ArrayLengthBasedRangeAccessorToken(val startIndex: Int, - val endIndex: Int? = null, - val offsetFromEnd: Int = 0) : Token { - override fun read(json: JsonNode): JsonNode { - val token = when (json) { - is RootLevelArrayNode -> { - val result = RootLevelArrayNode() - json.forEach { node -> - when(val nextNode = read(node)) { - is ArrayNode -> nextNode.forEach(result::add) - else -> when { - nextNode.isNotNullOrMissing() -> result.add(nextNode) - } - } - } - return result - } - is ArrayNode -> toMultiArrayAccessorToken(json) - else -> null - } - return token?.read(json) ?: RootLevelArrayNode() - } - - /** - * We know the size of the array during runtime so we can recreate the MultiArrayAccessorToken to read the values - */ - fun toMultiArrayAccessorToken(json: ArrayNode): MultiArrayAccessorToken? { - val size = json.size() - val start = if (startIndex < 0) { - val start = size + startIndex - if (start < 0) 0 else start // even if we're out of bounds at start, always start from first item - } else startIndex - - // use endIndex if we have it, otherwise calculate from json array length - val endInclusive = if (endIndex != null) { - endIndex - 1 - } else size + offsetFromEnd - 1 - - return if (start in 0..endInclusive) { - MultiArrayAccessorToken(IntRange(start, endInclusive).toList()) - } else null - } -} - -/** - * Accesses value at [key] from [ObjectNode] - * - * @param key key to access - */ -internal data class ObjectAccessorToken(val key: String) : Token { - override fun read(json: JsonNode): JsonNode? { - return read(json, key) - } - - companion object { - fun read(json: JsonNode, key: String): JsonNode? { - return when (json) { - is ObjectNode -> json.getValueIfNotNullOrMissing(key) - is RootLevelArrayNode -> { - // we're at root level and can get children from objects - val result = RootLevelArrayNode() - json.forEach { node -> - if(node is ObjectNode) { - node.getValueIfNotNullOrMissing(key)?.let { result.add(it) } - } - } - result - } - // ArrayNode should return null, unless it's the RootLevelArrayNode. This is intentional - // everything else is scalar and not accessible - else -> null - } - } - } -} - -/** - * Accesses values at [keys] from [ObjectNode]. When read, value returned will be [ObjectNode] - * containing key/value pairs requested. Keys that are null or don't exist won't be added in Object - * - * @param keys keys to access for which key/values to return - */ -internal data class MultiObjectAccessorToken(val keys: List) : Token { - override fun read(json: JsonNode): JsonNode { - - return when (json) { - is ObjectNode -> { - // Going from an object to a list always creates a root level list - val result = RootLevelArrayNode() - keys.forEach { - json.getValueIfNotNullOrMissing(it)?.let(result::add) - } - result - } - is RootLevelArrayNode -> { - val result = RootLevelArrayNode() - json.forEach { node -> - keys.forEach { key -> - ObjectAccessorToken.read(node, key)?.let { - if (it.isNotNullOrMissing()) { - result.add(it) - } - } - } - } - result - } - else -> RootLevelArrayNode() - } - } -} - -/** - * Recursive scan for values with keys in [targetKeys] list. Returns a [ArrayNode] containing values found. - * - * @param targetKeys keys to find values for - */ -internal data class DeepScanObjectAccessorToken(val targetKeys: List) : Token { - private fun scan(node: JsonNode, result: ArrayNode) { - when (node) { - is ObjectNode -> { - // first add all values from keys requested to our result - targetKeys.forEach { key -> - ObjectAccessorToken.read(node, key)?.let { - if (it.isNotNullOrMissing()) { - result.add(it) - } - } - } - - // recursively scan all underlying objects/arrays - node.forEach { - if (it.isNotNullOrMissing()) { - scan(it, result) - } - } - } - is ArrayNode -> { - node.forEach { - if (it.isNotNullOrMissing()) { - scan(it, result) - } - } - } - else -> {} - } - } - - override fun read(json: JsonNode): JsonNode { - val result = RootLevelArrayNode() - scan(json, result) - return result - } -} - -/** - * Recursive scan for values/objects/arrays found for all [indices] specified. Returns a [ArrayNode] containing results found. - * - * @param indices indices to retrieve values/objects for - */ -internal data class DeepScanArrayAccessorToken(val indices: List) : Token { - private fun scan(node: JsonNode, result: ArrayNode) { - when (node) { - is ObjectNode -> { - // traverse all key/value pairs and recursively scan underlying objects/arrays - node.forEach { - if (it.isNotNullOrMissing()) { - scan(it, result) - } - } - } - is RootLevelArrayNode -> { - // no need to add anything on root level, scan down next level - node.forEach { - if (it.isNotNullOrMissing()) { - scan(it, result) - } - } - } - is ArrayNode -> { - // first add all requested indices to our results - indices.forEach { index -> - ArrayAccessorToken(index).read(node)?.let { - if (it.isNotNullOrMissing()) { - result.add(it) - } - } - } - - // now recursively scan underlying objects/arrays - node.forEach { - if (it.isNotNullOrMissing()) { - scan(it, result) - } - } - } - else -> {} - } - } - - override fun read(json: JsonNode): JsonNode { - val result = RootLevelArrayNode() - scan(json, result) - return result - } -} - - -/** - * Recursive scan for values/objects/arrays from [ArrayNode] in range from [startIndex] to either [endIndex] or [offsetFromEnd] from end. - * When read, value returned will be ArrayNode of values at requested indices in order of values in range. Returns a [ArrayNode] containing results found. - * - * @param startIndex starting index of range, inclusive. Can be negative. - * @param endIndex ending index of range, exclusive. Null if using [offsetFromEnd] - * @param offsetFromEnd offset of values from end of array. 0 if using [endIndex] - */ -internal data class DeepScanLengthBasedArrayAccessorToken(val startIndex: Int, - val endIndex: Int? = null, - val offsetFromEnd: Int = 0) : Token { - private fun scan(node: JsonNode, result: ArrayNode) { - when (node) { - is ObjectNode -> { - // traverse all key/value pairs and recursively scan underlying objects/arrays - node.forEach { - if (it.isNotNullOrMissing()) { - scan(it, result) - } - } - } - is RootLevelArrayNode -> { - // no need to add anything on root level, scan down next level - node.forEach { - if (it.isNotNullOrMissing()) { - scan(it, result) - } - } - } - is ArrayNode -> { - ArrayLengthBasedRangeAccessorToken(startIndex, endIndex, offsetFromEnd) - .read(node).let { resultNode -> - val resultArray = resultNode as? ArrayNode - resultArray?.forEach { result.add(it) } - } - - // now recursively scan underlying objects/arrays - node.forEach { - if (it.isNotNullOrMissing()) { - scan(it, result) - } - } - } - else -> {} - } - } - - override fun read(json: JsonNode): JsonNode { - val result = RootLevelArrayNode() - scan(json, result) - return result - } -} - -/** - * Returns all values from an Object, or the same list - */ -internal class WildcardToken : Token { - override fun read(json: JsonNode): JsonNode { - return when (json) { - is ObjectNode -> { - val result = RootLevelArrayNode() - json.forEach { - if (it.isNotNullOrMissing()) { - result.add(it) - } - } - result - } - is ArrayNode -> { - if (json !is RootLevelArrayNode) { - // copy over children into our special ArrayNode to hold underlying items - RootLevelArrayNode(json) - } else { - val result = RootLevelArrayNode() - // iterate through each item and move everything up one level - json.forEach { node -> - when (node) { - is ObjectNode -> { - node.forEach { - if (it.isNotNullOrMissing()) { - result.add(it) - } - } - } - is ArrayNode -> { - // move all items from this node to result node - node.forEach { - if (it.isNotNullOrMissing()) { - result.add(it) - } - } - } - // anything else gets dropped since it's on rootmost level - } - } - result - } - } - else -> json - } - } - - override fun toString(): String = "WildcardToken" - override fun hashCode(): Int = toString().hashCode() - override fun equals(other: Any?): Boolean = other is WildcardToken -} - -internal class DeepScanWildcardToken : Token { - private fun scan(node: JsonNode, result: ArrayNode) { - when (node) { - is RootLevelArrayNode -> { - // no need to add anything on root level, scan down next level - node.forEach { - if (it.isNotNullOrMissing()) { - scan(it, result) - } - } - } - is ObjectNode, - is ArrayNode -> { - WildcardToken().read(node).let { nextNode -> - if (nextNode is ArrayNode) { - nextNode.forEach { - if (it.isNotNullOrMissing()) { - result.add(it) - } - } - } - } - - // now recursively scan underlying objects/arrays - node.forEach { - if (it.isNotNullOrMissing()) { - scan(it, result) - } - } - } - else -> {} - } - } - - override fun read(json: JsonNode): JsonNode { - val result = RootLevelArrayNode() - scan(json, result) - return result - } - - override fun toString(): String = "DeepScanWildcardToken" - override fun hashCode(): Int = toString().hashCode() - override fun equals(other: Any?): Boolean = other is DeepScanWildcardToken -} - -interface Token { - /** - * Takes in JsonNode and outputs next JsonNode or value by evaluating token against current object/array in path - */ - fun read(json: JsonNode): JsonNode? -} \ No newline at end of file diff --git a/src/main/kotlin/com/nfeld/jsonpathkt/cache/Cache.kt b/src/main/kotlin/com/nfeld/jsonpathkt/cache/Cache.kt deleted file mode 100644 index a3db998..0000000 --- a/src/main/kotlin/com/nfeld/jsonpathkt/cache/Cache.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.nfeld.jsonpathkt.cache - -import com.nfeld.jsonpathkt.JsonPath - -interface Cache { - /** - * Retrieve an instance of [JsonPath] containing the compiled path. - * - * @param path path string key for cache - * @return cached [JsonPath] instance or null if not cached - */ - fun get(path: String): JsonPath? - - /** - * Insert the given path and [JsonPath] as key/value pair into cache. - * - * @param path path string key for cache - * @param jsonPath instance of [JsonPath] containing compiled path - */ - fun put(path: String, jsonPath: JsonPath) -} \ No newline at end of file diff --git a/src/main/kotlin/com/nfeld/jsonpathkt/cache/CacheProvider.kt b/src/main/kotlin/com/nfeld/jsonpathkt/cache/CacheProvider.kt deleted file mode 100644 index 1126bec..0000000 --- a/src/main/kotlin/com/nfeld/jsonpathkt/cache/CacheProvider.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.nfeld.jsonpathkt.cache - -object CacheProvider { - - private var cache: Cache? = null - private var useDefault = true - - /** - * Consumer can set this to preferred max cache size. - */ - @JvmStatic - var maxCacheSize = 100 - - /** - * Set cache to custom implementation of [Cache]. - * - * @param newCache cache implementation to use, or null if no cache desired. - */ - @JvmStatic - fun setCache(newCache: Cache?) { - useDefault = false - cache = newCache - } - - internal fun getCache(): Cache? { - if (cache == null && useDefault) { - synchronized(this) { - if (cache == null) { - cache = createDefaultCache() - } - } - } - return cache - } - - private fun createDefaultCache(): Cache = LRUCache(maxCacheSize) -} \ No newline at end of file diff --git a/src/main/kotlin/com/nfeld/jsonpathkt/cache/LRUCache.kt b/src/main/kotlin/com/nfeld/jsonpathkt/cache/LRUCache.kt deleted file mode 100644 index 5f89599..0000000 --- a/src/main/kotlin/com/nfeld/jsonpathkt/cache/LRUCache.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.nfeld.jsonpathkt.cache - -import com.nfeld.jsonpathkt.JsonPath -import org.jetbrains.annotations.TestOnly -import java.util.LinkedHashMap - -internal class LRUCache(private val maxCacheSize: Int): Cache { - private val map = LRUMap() - - @Synchronized - override fun get(path: String): JsonPath? = map.get(path) - - @Synchronized - override fun put(path: String, jsonPath: JsonPath) { - map.put(path, jsonPath) - } - - @TestOnly - internal fun toList(): List> = map.toList() - - private inner class LRUMap : LinkedHashMap(INITIAL_CAPACITY, LOAD_FACTOR, true) { - override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): Boolean = size > maxCacheSize - } - - companion object { - private const val INITIAL_CAPACITY = 16 - private const val LOAD_FACTOR = 0.75f - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/nfeld/jsonpathkt/extension/JsonNode.kt b/src/main/kotlin/com/nfeld/jsonpathkt/extension/JsonNode.kt deleted file mode 100644 index d93dae9..0000000 --- a/src/main/kotlin/com/nfeld/jsonpathkt/extension/JsonNode.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.nfeld.jsonpathkt.extension - -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.node.ArrayNode -import com.fasterxml.jackson.databind.node.MissingNode -import com.fasterxml.jackson.databind.node.NullNode -import com.fasterxml.jackson.databind.node.ObjectNode -import com.nfeld.jsonpathkt.JsonPath - -inline fun JsonNode.read(jsonpath: String): T? { - return JsonPath(jsonpath).readFromJson(this) -} - -inline fun JsonNode.read(jsonpath: JsonPath): T? { - return jsonpath.readFromJson(this) -} - -internal inline fun JsonNode?.isNotNullOrMissing(): Boolean { - return this != null && this !is NullNode && this !is MissingNode -} - -internal inline fun ArrayNode.children(): List { - return map { it } -} - -internal inline fun ObjectNode.getValueIfNotNullOrMissing(key: String): JsonNode? { - val value = get(key) - return if (value.isNotNullOrMissing()) { - value - } else null -} - -internal inline fun ArrayNode.getValueIfNotNullOrMissing(index: Int): JsonNode? { - val value = get(index) - return if (value.isNotNullOrMissing()) { - value - } else null -} \ No newline at end of file diff --git a/src/main/kotlin/com/nfeld/jsonpathkt/util/JacksonUtil.kt b/src/main/kotlin/com/nfeld/jsonpathkt/util/JacksonUtil.kt deleted file mode 100644 index 28ee462..0000000 --- a/src/main/kotlin/com/nfeld/jsonpathkt/util/JacksonUtil.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.nfeld.jsonpathkt.util - -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.SerializationFeature -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper - -object JacksonUtil { - val mapper: ObjectMapper by lazy { - jacksonObjectMapper() - .configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true) - } -} - -internal inline fun createObjectNode() = JacksonUtil.mapper.createObjectNode() -internal inline fun createArrayNode() = JacksonUtil.mapper.createArrayNode() diff --git a/src/main/kotlin/com/nfeld/jsonpathkt/util/RootLevelArrayNode.kt b/src/main/kotlin/com/nfeld/jsonpathkt/util/RootLevelArrayNode.kt deleted file mode 100644 index 3d63555..0000000 --- a/src/main/kotlin/com/nfeld/jsonpathkt/util/RootLevelArrayNode.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.nfeld.jsonpathkt.util - -import com.fasterxml.jackson.databind.node.ArrayNode -import com.nfeld.jsonpathkt.extension.children - -internal class RootLevelArrayNode : ArrayNode { - - /** - * Creates an empty RootLevelArrayNode - */ - constructor() : super(JacksonUtil.mapper.nodeFactory) - - /** - * Creates a RootLevelArrayNode with the children of the provided ArrayNode - * */ - constructor(arrayNode: ArrayNode) : super(JacksonUtil.mapper.nodeFactory, arrayNode.children()) -} diff --git a/src/test/kotlin/com/nfeld/jsonpathkt/BenchmarkTest.kt b/src/test/kotlin/com/nfeld/jsonpathkt/BenchmarkTest.kt deleted file mode 100644 index 782c6aa..0000000 --- a/src/test/kotlin/com/nfeld/jsonpathkt/BenchmarkTest.kt +++ /dev/null @@ -1,170 +0,0 @@ -package com.nfeld.jsonpathkt - -import com.jayway.jsonpath.Configuration -import com.jayway.jsonpath.spi.cache.NOOPCache -import com.jayway.jsonpath.spi.json.JacksonJsonProvider -import com.nfeld.jsonpathkt.cache.CacheProvider -import io.kotest.core.spec.style.StringSpec - -private const val DEFAULT_RUNS = 30 -private const val DEFAULT_CALLS_PER_RUN = 80000 -private var printReadmeFormat = false - -private fun benchmark(callsPerRun: Int = DEFAULT_CALLS_PER_RUN, runs: Int = DEFAULT_RUNS, f: () -> Unit): Long { - // warmup - f() - - val times = mutableListOf() - - for (i in 0 until runs) { - val t1 = timestamp - for (k in 0 until callsPerRun) { - f() - } - val t2 = timestamp - times.add(t2 - t1) - } - - return times.average().toLong() -} - -private fun benchmarkJsonPathKt(path: String, callsPerRun: Int = DEFAULT_CALLS_PER_RUN, runs: Int = DEFAULT_RUNS): Long { - val json = JsonPath.parse(LARGE_JSON)!! // pre-parse json -// println("our result: " + JsonPath(path).readFromJson(json).toString()) - return benchmark(callsPerRun, runs) { JsonPath(path).readFromJson(json) } -} - -private fun benchmarkJsonPath(path: String, callsPerRun: Int = DEFAULT_CALLS_PER_RUN, runs: Int = DEFAULT_RUNS): Long { - val jaywayConfig = Configuration.defaultConfiguration().jsonProvider(JacksonJsonProvider()) - val documentContext = com.jayway.jsonpath.JsonPath.parse(LARGE_JSON, jaywayConfig) // pre-parse json -// println("jayway result: " + documentContext.read(path).toString()) - return benchmark(callsPerRun, runs) { documentContext.read(path) } -} - -private fun runBenchmarksAndPrintResults(path: String, callsPerRun: Int = DEFAULT_CALLS_PER_RUN, runs: Int = DEFAULT_RUNS) { - // reset caches to initial position, default on - resetCaches() - - // first benchmarks will be using caches - val kt = benchmarkJsonPathKt(path, callsPerRun, runs) - val other = benchmarkJsonPath(path, callsPerRun, runs) - - // now disable caches - CacheProvider.setCache(null) - resetJaywayCacheProvider() - com.jayway.jsonpath.spi.cache.CacheProvider.setCache(NOOPCache()) - val ktNoCache = benchmarkJsonPathKt(path, callsPerRun, runs) - val otherNoCache = benchmarkJsonPath(path, callsPerRun, runs) - - if (printReadmeFormat) { - println("| $path | $ktNoCache ms *($kt ms w/ cache)* | $otherNoCache ms *($other ms w/ cache)* |") - } else { - println("$path kt: ${kt}, jsonpath: $other Without caches: kt: ${ktNoCache}, jsonpath: $otherNoCache") - } -} - -private fun resetCaches() { - resetCacheProvider() - resetJaywayCacheProvider() -} - -class BenchmarkTest : StringSpec({ - - beforeSpec { - println("Setting up BenchmarkTest") - - printReadmeFormat = System.getProperty("readmeFormat")?.toBoolean() ?: false - } - - "benchmark deep path" { - runBenchmarksAndPrintResults("$[0].friends[1].other.a.b['c']") - } - - "benchmark shallow path" { - runBenchmarksAndPrintResults("$[2]._id") - } - - "benchmark compiling path" { - - fun compile(path: String) { - resetCaches() - - // first with caches - val kt = benchmark { JsonPath(path) } - val other = benchmark { com.jayway.jsonpath.JsonPath.compile(path) } - - // now disable caches - CacheProvider.setCache(null) - resetJaywayCacheProvider() - com.jayway.jsonpath.spi.cache.CacheProvider.setCache(NOOPCache()) - - val ktNoCache = benchmark { JsonPath(path) } - val otherNoCache = benchmark { com.jayway.jsonpath.JsonPath.compile(path) } - - val numTokens = PathCompiler.compile(path).size - val name = "${path.length} chars, $numTokens tokens" - - if (printReadmeFormat) { - println("| $name | $ktNoCache ms *($kt ms w/ cache)* | $otherNoCache ms *($other ms w/ cache)* |") - } else { - println("$name kt: ${kt}, jsonpath: $other Without caches: kt: ${ktNoCache}, jsonpath: $otherNoCache") - } - } - - compile("$.hello") - compile("$.hello.world[0]") - compile("$[0].friends[1].other.a.b['c']") - compile("$[0].friends[1].other.a.b['c'][5].niko[2].hello.world[6][9][0].id") - compile("$[0].friends[1]..other[2].a.b['c'][5].niko[2]..hello[0].world[6][9]..['a','b','c'][0].id") - } - - "benchmark deep scans" { - val callsPerRun = 20000 - val runs = 10 - runBenchmarksAndPrintResults("$..name", callsPerRun, runs) - runBenchmarksAndPrintResults("$..['email','name']", callsPerRun, runs) - runBenchmarksAndPrintResults("$..[1]", callsPerRun, runs) - } - - "benchmark deep scan ranges" { - val callsPerRun = 20000 - val runs = 10 - runBenchmarksAndPrintResults("$..[:2]", callsPerRun, runs) - runBenchmarksAndPrintResults("$..[2:]", callsPerRun, runs) - - // jayway jsonpath gives empty response for this so not valid comparison - // runBenchmarksAndPrintResults("$..[1:-1]", callsPerRun, runs) - } - - "benchmark array access from end element" { - runBenchmarksAndPrintResults("$[0]['tags'][-3]") - } - - "benchmark array range from start" { - runBenchmarksAndPrintResults("$[0]['tags'][:3]") - } - - "benchmark array range to end element" { - runBenchmarksAndPrintResults("$[0]['tags'][3:]") - } - - "benchmark array range" { - runBenchmarksAndPrintResults("$[0]['tags'][3:5]") - } - - "benchmark multi array access" { - runBenchmarksAndPrintResults("$[0]['tags'][0,3,5]") - } - - "benchmark multi object access" { - runBenchmarksAndPrintResults("$[0]['latitude','longitude','isActive']") - } - - "benchmark wildcard" { - runBenchmarksAndPrintResults("$[0]['tags'].*") - } - - "benchmark recursive wildcard" { - runBenchmarksAndPrintResults("$[0]..*") - } -}) \ No newline at end of file diff --git a/src/test/kotlin/com/nfeld/jsonpathkt/JsonPathTest.kt b/src/test/kotlin/com/nfeld/jsonpathkt/JsonPathTest.kt deleted file mode 100644 index 0631f13..0000000 --- a/src/test/kotlin/com/nfeld/jsonpathkt/JsonPathTest.kt +++ /dev/null @@ -1,574 +0,0 @@ -package com.nfeld.jsonpathkt - -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.node.* -import com.nfeld.jsonpathkt.cache.CacheProvider -import com.nfeld.jsonpathkt.extension.read -import com.nfeld.jsonpathkt.util.JacksonUtil -import com.nfeld.jsonpathkt.util.RootLevelArrayNode -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.shouldBe - -data class PojoClass1(val key: String) -data class PojoClass2(val key: List) -data class PojoClass3(val key: Map, val default: Int) - -class JsonPathTest : DescribeSpec({ - beforeTest() { - CacheProvider.setCache(null) - } - - describe("Core parse tests") { - it("should be null on parse failure") { - JsonPath.parse("5{}") shouldBe null - JsonPath.parse("5[]") shouldBe null - JsonPath.parse("$") shouldBe null - JsonPath.parse("{") shouldBe null - JsonPath.parse("") shouldBe null - JsonPath.parse("{]") shouldBe null - JsonPath.parse("[}") shouldBe null - JsonPath.parse("null") shouldBe null - JsonPath.parse(null) shouldBe null - } - - it("should be null if root node is Missing or Null") { - JsonPath("$").readFromJson(JacksonUtil.mapper.missingNode()) shouldBe null - JsonPath("$").readFromJson(JacksonUtil.mapper.nullNode()) shouldBe null - } - - it("should be null when parsing root string but without quotes") { - JsonPath.parse("hello") shouldBe null - } - - it("should parse root string with quotes") { - JsonPath.parse(""""hello"""") shouldBe TextNode("hello") - JsonPath.parse(""""hello"""")!!.read("$") shouldBe "hello" - } - - it("should parse root values other than String") { - JsonPath.parse("4") shouldBe IntNode(4) - JsonPath.parse("4")!!.read("$") shouldBe 4 - JsonPath.parse("4.76") shouldBe DoubleNode(4.76) - JsonPath.parse("4.76")!!.read("$") shouldBe 4.76 - JsonPath.parse("true") shouldBe BooleanNode.TRUE - JsonPath.parse("true")!!.read("$") shouldBe true - JsonPath.parse("false") shouldBe BooleanNode.FALSE - JsonPath.parse("false")!!.read("$") shouldBe false - } - - it("should be able to get ObjectNode") { - JsonPath.parse(SMALL_JSON)!!.read("$") shouldBe readTree(SMALL_JSON) - } - - it("should be able to get ArrayNode") { - JsonPath.parse(SMALL_JSON_ARRAY)!!.read("$") shouldBe readTree(SMALL_JSON_ARRAY) - } - - it("should be able to get inner ObjectNodes") { - val json = """[{"outer": {"inner": 9} }]""" - JsonPath.parse(json)!!.read("$[0]") shouldBe readTree(json)[0] - JsonPath.parse(json)!!.read("$[0].outer") shouldBe readTree(json)[0]["outer"] - } - - it("should get values deep in JSON") { - JsonPath.parse(LARGE_JSON)!!.read("$[0].friends[1].other.a.b['c']") shouldBe "yo" - JsonPath.parse(LARGE_JSON)!!.read("$[0].friends[-1]['name']") shouldBe "Harrell Pratt" - } - - it("should preserve order") { - JsonPath.parse(BOOKS_JSON)!!.read>("$.store..price") shouldBe listOf(8.95, 12.99, 8.99, 22.99, 19.95) - JsonPath.parse("""{"d": 4, "f": 6, "e": 5, "a": 1, "b": 2, "c": 3}""")!!.read>("$.*") shouldBe listOf(4,6,5,1,2,3) - } - - it("returned RootLevelArrayNode should be ArrayNode since internal") { - JsonPath.parse("""{"d": 4, "f": 6, "e": 5, "a": 1, "b": 2, "c": 3}""")!!.read("$.*").toString() shouldBe "[4,6,5,1,2,3]" - (JsonPath.parse("""{"d": 4, "f": 6, "e": 5, "a": 1, "b": 2, "c": 3}""")!!.read("$.*") is ArrayNode) shouldBe true - (JsonPath.parse("""{"d": 4, "f": 6, "e": 5, "a": 1, "b": 2, "c": 3}""")!!.read("$.*") is RootLevelArrayNode) shouldBe false - (JsonPath.parse("""[1,2,3,4]""")!!.read("$.*") is RootLevelArrayNode) shouldBe false - } - } - - describe("Type casting") { - val json = """{ - "int": 5, - "string": "hello", - "boolean": true, - "double": 6.35, - "char": "a" - }""" - - it("casts to Int") { - JsonPath.parse(json)!!.read("$.int") shouldBe 5 - JsonPath.parse(json)!!.read("$.string") shouldBe null - JsonPath.parse(json)!!.read("$.boolean") shouldBe null - JsonPath.parse(json)!!.read("$.double") shouldBe 6 - JsonPath.parse(json)!!.read("$.char") shouldBe null - } - - it("casts to Double") { - JsonPath.parse(json)!!.read("$.int") shouldBe 5.0 - JsonPath.parse(json)!!.read("$.string") shouldBe null - JsonPath.parse(json)!!.read("$.boolean") shouldBe null - JsonPath.parse(json)!!.read("$.double") shouldBe 6.35 - JsonPath.parse(json)!!.read("$.char") shouldBe null - } - - it("casts to Boolean") { - JsonPath.parse(json)!!.read("$.int") shouldBe true - JsonPath.parse(json)!!.read("$.string") shouldBe null - JsonPath.parse(json)!!.read("$.boolean") shouldBe true - JsonPath.parse(json)!!.read("$.double") shouldBe null - JsonPath.parse(json)!!.read("$.char") shouldBe null - } - - it("casts to Char") { - JsonPath.parse(json)!!.read("$.int") shouldBe 5.toChar() - JsonPath.parse(json)!!.read("$.string") shouldBe null - JsonPath.parse(json)!!.read("$.boolean") shouldBe null - JsonPath.parse(json)!!.read("$.double") shouldBe null - JsonPath.parse(json)!!.read("$.char") shouldBe 'a' - } - - it("everything can be cast to String") { - JsonPath.parse(json)!!.read("$.int") shouldBe "5" - JsonPath.parse(json)!!.read("$.string") shouldBe "hello" - JsonPath.parse(json)!!.read("$.boolean") shouldBe "true" - JsonPath.parse(json)!!.read("$.double") shouldBe "6.35" - JsonPath.parse(json)!!.read("$.char") shouldBe "a" - } - - it("should cast POJO") { - JsonPath.parse("""{"key": "value"}""")!!.read("$") shouldBe PojoClass1(key = "value") - JsonPath.parse("""{"key": [1, "random", null, 1.765]}""")!!.read("$") shouldBe PojoClass2(key = listOf(1, "random", null, 1.765)) - JsonPath.parse("""{"key": { "a": 1, "b": 2 }, "default": 3}""")!!.read("$") shouldBe PojoClass3(key = mapOf("a" to 1, "b" to 2), default = 3) - } - } - - describe("Object accessors") { - it("should be null of key doesn't exist") { - JsonPath.parse(SMALL_JSON)!!.read("$.unknownkey") shouldBe null - JsonPath.parse(SMALL_JSON)!!.read("$['unknownkey']") shouldBe null - } - - it("should get value if key exists") { - JsonPath.parse(SMALL_JSON)!!.read("$.key") shouldBe 5 - JsonPath.parse(SMALL_JSON)!!.read("$['key']") shouldBe 5 - } - - it("should be null if reading null value") { - JsonPath.parse("""{"key":null}""")!!.read("$['key']") shouldBe null - } - - it("should access empty string key and other uncommon keys") { - JsonPath.parse("""{"":4}""")!!.read("$['']") shouldBe 4 - JsonPath.parse("""{"":4}""")!!.read("$[\"\"]") shouldBe 4 - JsonPath.parse("""{"'":4}""")!!.read("$[\"'\"]") shouldBe 4 - JsonPath.parse("""{"'":4}""")!!.read("$['\\'']") shouldBe 4 - JsonPath.parse("""{"\"": 4}""")!!.read("""$["\""]""") shouldBe 4 - JsonPath.parse("""{"\"": 4}""")!!.read("""$['"']""") shouldBe 4 - JsonPath.parse("""{"\\": 4}""")!!.read("""$['\\']""") shouldBe 4 - } - - it("should read object keys that have numbers and/or symbols") { - val key = "!@#\$%^&*()_-+=[]{}|:;<,>.?`~" // excluding ' - val json = """ - { - "key1": "a", - "ke2y": "b", - "ke3%y": "c", - "1234": "d", - "12$34": "e", - "abc{}3d": "f", - "$key": "g" - } - """ - JsonPath.parse(json)!!.read("$.key1") shouldBe "a" - JsonPath.parse(json)!!.read("$['key1']") shouldBe "a" - JsonPath.parse(json)!!.read("$.ke2y") shouldBe "b" - JsonPath.parse(json)!!.read("$['ke2y']") shouldBe "b" - JsonPath.parse(json)!!.read("$.ke3%y") shouldBe "c" - JsonPath.parse(json)!!.read("$['ke3%y']") shouldBe "c" - JsonPath.parse(json)!!.read("$.1234") shouldBe "d" - JsonPath.parse(json)!!.read("$['1234']") shouldBe "d" - JsonPath.parse(json)!!.read("$.12$34") shouldBe "e" - JsonPath.parse(json)!!.read("$['12$34']") shouldBe "e" - JsonPath.parse(json)!!.read("$.abc{}3d") shouldBe "f" - JsonPath.parse(json)!!.read("$['abc{}3d']") shouldBe "f" - JsonPath.parse(json)!!.read("$['$key']") shouldBe "g" - } - - it("should be null on unsupported selectors on objects") { - JsonPath.parse(SMALL_JSON)!!.read("$[:]") shouldBe null - } - - it("should read key from list if list item is an object") { - JsonPath.parse("""[{"key": "ey"}, {"key": "bee"}, {"key": "see"}]""")!!.read("$.key") shouldBe null - JsonPath.parse("""[{"key": "ey"}, {"key": "bee"}, {"key": "see"}]""")!!.read("$.*.key").toString() shouldBe """["ey","bee","see"]""" - JsonPath.parse("""[{"key": "ey"}, {"key": "bee"}, {"key": "see"}]""")!!.read("$[0,2].key").toString() shouldBe """["ey","see"]""" - JsonPath.parse(""" - { - "one": {"key": "value"}, - "two": {"k": "v"}, - "three": {"some": "more", "key": "other value"} - } - """)!!.read("$['one','three'].key").toString() shouldBe """["value","other value"]""" - - JsonPath.parse("""[{"a": 1},{"a": 1}]""")!!.read("$[*].a").toString() shouldBe """[1,1]""" - JsonPath.parse("""[{"a": 1},{"a": 1}]""")!!.read("$.*.a").toString() shouldBe """[1,1]""" - } - - describe("Multi object accessors") { - it("should get list of scalars") { - JsonPath.parse("""{"key": "value", "another": "entry"}""")!!.read>("$['key','another']") shouldBe listOf("value", "entry") - } - - it("should return empty list of reading from a list that's not root list") { - JsonPath.parse("""[{"key": "value", "another": "entry"}]""")!!.read("$['key','another']").toString() shouldBe "[]" - JsonPath.parse("""[{"key": "ey", "other": 1}, {"key": "bee"}, {"key": "see", "else": 3}]""")!!.read("$['key','other']").toString() shouldBe "[]" - } - - it("should read obj keys from root list") { - JsonPath.parse("""[{"key": "value", "another": "entry"}]""")!!.read>("$.*['key','another']") shouldBe listOf("value", "entry") - JsonPath.parse("""[{"key": "ey", "other": 1}, {"key": "bee"}, {"key": "see", "else": 3}]""")!!.read("$.*['key','other']").toString() shouldBe """["ey",1,"bee","see"]""" - } - - it("should get all 3 keys") { - JsonPath.parse(LARGE_JSON)!!.read>("$[0]['latitude','longitude','isActive']") shouldBe listOf(-85.888651, 38.287152, true) - } - - it("should get only the key/value pairs when found") { - JsonPath.parse(LARGE_JSON)!!.read>("$[0]['latitude','longitude', 'unknownkey']") shouldBe listOf( -85.888651, 38.287152) - } - } - } - - describe("Array accessors") { - it("should be null of index out of bounds") { - JsonPath.parse(SMALL_JSON_ARRAY)!!.read("$[43]") shouldBe null - JsonPath.parse(SMALL_JSON_ARRAY)!!.read("$[-43]") shouldBe null - } - - it("should get value if value exists at index") { - JsonPath.parse(SMALL_JSON_ARRAY)!!.read("$[2]") shouldBe 3 - JsonPath.parse(SMALL_JSON_ARRAY)!!.read("$[0]") shouldBe 1 - } - - it("should get value from ends") { - JsonPath.parse(SMALL_JSON_ARRAY)!!.read("$[-2]") shouldBe 4 - JsonPath.parse(SMALL_JSON_ARRAY)!!.read("$[-4]") shouldBe 2 - JsonPath.parse(LARGE_JSON)!!.read("$[0]['tags'][-1]") shouldBe "qui" - JsonPath.parse(LARGE_JSON)!!.read("$[0]['tags'][-3]") shouldBe "cillum" - } - - it("-0 should get first item in array") { - JsonPath.parse(SMALL_JSON_ARRAY)!!.read("$[-0]") shouldBe 1 - } - - it("should return null if used on JSON object") { - JsonPath.parse("""{"key":3}""")!!.read("$[3]") shouldBe null - } - - it("should return null if used on a scalar other than String") { - JsonPath.parse("5")!!.read("$[0]") shouldBe null - JsonPath.parse("5.34")!!.read("$[0]") shouldBe null - JsonPath.parse("true")!!.read("$[0]") shouldBe null - JsonPath.parse("false")!!.read("$[0]") shouldBe null - } - - it("should get character at index if String scalar") { - JsonPath.parse(""""hello"""")!!.read("$[0]") shouldBe "h" - } - - describe("Multi array accessors") { - it("should get first, fourth, and sixth items") { - JsonPath.parse(LARGE_JSON)!!.read>("$[0]['tags'][0,3,5]") shouldBe listOf("occaecat","labore","laboris") - } - - it("should get only the items with valid index") { - JsonPath.parse(LARGE_JSON)!!.read>("$[0]['tags'][0,30,50]") shouldBe listOf("occaecat") - } - - it("should return empty list if used on JSON object") { - JsonPath.parse("""{"key":3}""")!!.read("$[3,4]")?.toString() shouldBe "[]" - } - } - - describe("Array ranges") { - it("should handle array range from start") { - JsonPath.parse(LARGE_JSON)!!.read>("$[0]['tags'][:3]") shouldBe listOf("occaecat","mollit","ullamco") - JsonPath.parse(LARGE_JSON)!!.read>("$[0]['tags'][:-4]") shouldBe listOf("occaecat","mollit","ullamco") - } - - it("should handle array range to end") { - JsonPath.parse(LARGE_JSON)!!.read>("$[0]['tags'][5:]") shouldBe listOf("laboris","qui") - JsonPath.parse(LARGE_JSON)!!.read>("$[0]['tags'][-2:]") shouldBe listOf("laboris","qui") - } - - it("should handle specified range, exclusive at end") { - JsonPath.parse(LARGE_JSON)!!.read>("$[0]['tags'][3:5]") shouldBe listOf("labore","cillum") - JsonPath.parse(LARGE_JSON)!!.read>("$[0]['tags'][3:-1]") shouldBe listOf("labore","cillum","laboris") - JsonPath.parse(LARGE_JSON)!!.read>("$[0]['tags'][-6:4]") shouldBe listOf("mollit","ullamco","labore") - JsonPath.parse(LARGE_JSON)!!.read>("$[0]['tags'][-3:-1]") shouldBe listOf("cillum","laboris") - } - - it("should return range items up to end if end index out of bounds") { - JsonPath.parse(LARGE_JSON)!!.read>("$[0]['tags'][5:30]") shouldBe listOf("laboris","qui") - } - - it("should return range items up from start if start index out of bounds") { - JsonPath.parse("""["first", "second", "third"]""")!!.read>("$[-4:]") shouldBe listOf("first", "second", "third") - } - - it("should return empty list if used on JSON object") { - JsonPath.parse("""{"key":3}""")!!.read("$[1:3]")?.toString() shouldBe "[]" - } - - it("should get all items in list") { - JsonPath.parse("""["first", "second"]""")!!.read>("$[:]") shouldBe listOf("first", "second") - JsonPath.parse("""["first", "second"]""")!!.read>("$[0:]") shouldBe listOf("first", "second") - JsonPath.parse("""["first", "second"]""")!!.read>("$") shouldBe listOf("first", "second") - - val expected = listOf( - mapOf( - "name" to "Thomas", - "age" to 13 - ), - mapOf( - "name" to "Mila", - "age" to 18 - ), - mapOf( - "name" to "Konstantin", - "age" to 29, - "nickname" to "Kons" - ), - mapOf( - "name" to "Tracy", - "age" to 4 - ) - ) - JsonPath.parse(FAMILY_JSON)!!.read>>("$.family.children[:]") shouldBe expected - JsonPath.parse(FAMILY_JSON)!!.read>>("$.family.children[0:]") shouldBe expected - } - - it("[:] combos") { - val json = """[{"c":"cc1","d":"dd1","e":"ee1"},{"c":"cc2","d":"dd2","e":"ee2"}]""" - JsonPath.parse(json)!!.read("$[:]").toString() shouldBe """[{"c":"cc1","d":"dd1","e":"ee1"},{"c":"cc2","d":"dd2","e":"ee2"}]""" - JsonPath.parse(json)!!.read("$[:]['c']").toString() shouldBe """["cc1","cc2"]""" - JsonPath.parse(json)!!.read("$[:]['c','d']").toString() shouldBe """["cc1","dd1","cc2","dd2"]""" - JsonPath.parse(json)!!.read("$..[:]").toString() shouldBe """[{"c":"cc1","d":"dd1","e":"ee1"},{"c":"cc2","d":"dd2","e":"ee2"}]""" - JsonPath.parse(json)!!.read("$.*[:]").toString() shouldBe """[]""" - - val json2 = "[1,[2],[3,4],[5,6,7]]" - JsonPath.parse(json2)!!.read("$[:]").toString() shouldBe """[1,[2],[3,4],[5,6,7]]""" - JsonPath.parse(json2)!!.read("$[:][0]").toString() shouldBe """[2,3,5]""" - JsonPath.parse(json2)!!.read("$[:][1]").toString() shouldBe """[4,6]""" - JsonPath.parse(json2)!!.read("$.*[:]").toString() shouldBe """[2,3,4,5,6,7]""" - JsonPath.parse(json2)!!.read("$..[:]").toString() shouldBe """[1,[2],[3,4],[5,6,7],2,3,4,5,6,7]""" - JsonPath.parse(json2)!!.read("$..[:].*").toString() shouldBe """[2,3,4,5,6,7]""" - } - } - } - - describe("List collections") { - it("should include nulls") { - JsonPath.parse("""{"key": [1, "random", null, 1.765]}""")!!.read>("$.key") shouldBe listOf(1, "random", null, 1.765) - } - - it("should be String collection") { - JsonPath.parse(LARGE_JSON)!!.read>("$[0].tags") shouldBe listOf("occaecat","mollit","ullamco","labore","cillum","laboris","qui") - } - - it("should be Int collection") { - JsonPath.parse(LARGE_JSON)!!.read>("$[5].nums") shouldBe listOf(1,2,3,4,5) - } - - it("should be Long collection") { - JsonPath.parse(LARGE_JSON)!!.read>("$[5].nums") shouldBe listOf(1L,2L,3L,4L,5L) - } - - it("should get a Set collection to remove duplicates") { - JsonPath.parse("""[1,2,3,1,2,4,5]""")!!.read>("$") shouldBe setOf(1,2,3,4,5) - } - } - - describe("Map objects") { - it("should be Map") { - JsonPath.parse("""{"a": {"b": "yo"}}}""")!!.read>("$") shouldBe mapOf("a" to mapOf("b" to "yo")) - JsonPath.parse("""{"a": {"b": "yo"}}}""")!!.read>("$.a") shouldBe mapOf("b" to "yo") - } - } - - describe("Deep scans") { - it("should get String list") { - val expected = listOf("Salazar Casey","Kathrine Osborn","Vonda Howe","Harrell Pratt","Porter Cummings", - "Mason Leach","Spencer Valenzuela","Hope Medina","Marie Hampton","Felecia Bright", - "Maryanne Wiggins","Marylou Caldwell","Mari Pugh","Rios Norton","Judy Good","Rosetta Stanley", - "Margret Quinn","Lora Cotton","Gaines Henry","Dorothea Irwin") - JsonPath.parse(LARGE_JSON)!!.read>("$..name") shouldBe expected - JsonPath.parse(LARGE_JSON)!!.read>("$..name") shouldBe expected - } - - it("should get Double list") { - JsonPath.parse(LARGE_JSON)!!.read>("$..latitude") shouldBe listOf(-85.888651, 71.831798, 78.266157, -10.214391, 32.293366) - JsonPath.parse(LARGE_JSON)!!.read>("$..['latitude']") shouldBe listOf(-85.888651, 71.831798, 78.266157, -10.214391, 32.293366) - } - - it("should get ArrayNode") { - val expected = """[["occaecat","mollit","ullamco","labore","cillum","laboris","qui"],["aliquip","cillum","qui","ut","ea","eu","reprehenderit"],["nulla","elit","ipsum","pariatur","ullamco","ut","sint"],["fugiat","sit","ad","voluptate","officia","aute","duis"],["est","dolor","dolore","exercitation","minim","dolor","pariatur"]]""" - JsonPath.parse(LARGE_JSON)!!.read("$..tags")?.toString() shouldBe expected - JsonPath.parse(LARGE_JSON)!!.read("$..['tags']")?.toString() shouldBe expected - } - - it("should get from longer path") { - JsonPath.parse(LARGE_JSON)!!.read>("$[2]..name") shouldBe listOf("Marie Hampton", "Felecia Bright", "Maryanne Wiggins", "Marylou Caldwell") - JsonPath.parse(LARGE_JSON)!!.read>("$[2]..['name']") shouldBe listOf("Marie Hampton", "Felecia Bright", "Maryanne Wiggins", "Marylou Caldwell") - } - - it("should scan to get the first item of all sublists") { - JsonPath.parse(LARGE_JSON)!!.read("$[2]..[0]").toString() shouldBe """["nulla",{"id":0,"name":"Felecia Bright","other":{"a":{"b":{"c":"yo"}}}}]""" - } - - it("should scan to get the last item of all sublists") { - JsonPath.parse(LARGE_JSON)!!.read("$[2]..[-1]").toString() shouldBe """["sint",{"id":2,"name":"Marylou Caldwell","other":{"a":{"b":{"c":"yo"}}}}]""" - } - - it("should scan to get the first and third items of all sublists") { - JsonPath.parse(LARGE_JSON)!!.read("$[2]..[0,2]").toString() shouldBe """["nulla","ipsum",{"id":0,"name":"Felecia Bright","other":{"a":{"b":{"c":"yo"}}}},{"id":2,"name":"Marylou Caldwell","other":{"a":{"b":{"c":"yo"}}}}]""" - } - - it("should scan to get the first and second from last items of all sublists") { - JsonPath.parse(LARGE_JSON)!!.read("$[2]..[0, -2]").toString() shouldBe """["nulla","ut",{"id":0,"name":"Felecia Bright","other":{"a":{"b":{"c":"yo"}}}},{"name":"Maryanne Wiggins","other":{"a":{"b":{"c":"yo"}}}}]""" - } - - it("should scan to get the first and second (range) items of all sublists") { - JsonPath.parse(LARGE_JSON)!!.read("$[2]..[0:2]").toString() shouldBe """["nulla","elit",{"id":0,"name":"Felecia Bright","other":{"a":{"b":{"c":"yo"}}}},{"name":"Maryanne Wiggins","other":{"a":{"b":{"c":"yo"}}}}]""" - } - - it("should scan to get the third and all following items of all sublists") { - JsonPath.parse(LARGE_JSON)!!.read("$[2]..[2:]").toString() shouldBe """["ipsum","pariatur","ullamco","ut","sint",{"id":2,"name":"Marylou Caldwell","other":{"a":{"b":{"c":"yo"}}}}]""" - } - - it("should scan to get all items except for last item of all sublists") { - JsonPath.parse(LARGE_JSON)!!.read("$[2]..[:-1]").toString() shouldBe """["nulla","elit","ipsum","pariatur","ullamco","ut",{"id":0,"name":"Felecia Bright","other":{"a":{"b":{"c":"yo"}}}},{"name":"Maryanne Wiggins","other":{"a":{"b":{"c":"yo"}}}}]""" - } - - it("should scan to get all items starting from second to last of all sublists") { - JsonPath.parse(LARGE_JSON)!!.read("$[2]..[-2:]").toString() shouldBe """["ut","sint",{"name":"Maryanne Wiggins","other":{"a":{"b":{"c":"yo"}}}},{"id":2,"name":"Marylou Caldwell","other":{"a":{"b":{"c":"yo"}}}}]""" - } - - it("should scan to get all items between first and last of all sublists (both sides exclusive)") { - JsonPath.parse(LARGE_JSON)!!.read("$[2]..[1:-1]").toString() shouldBe """["elit","ipsum","pariatur","ullamco","ut",{"name":"Maryanne Wiggins","other":{"a":{"b":{"c":"yo"}}}}]""" - } - - it("should scan to get all items from second to last, to 4th item of all sublists") { - JsonPath.parse(LARGE_JSON)!!.read("$[2]..[-2:5]").toString() shouldBe """[{"name":"Maryanne Wiggins","other":{"a":{"b":{"c":"yo"}}}},{"id":2,"name":"Marylou Caldwell","other":{"a":{"b":{"c":"yo"}}}}]""" - } - - it("should scan to get second from last item of all sublists") { - JsonPath.parse(LARGE_JSON)!!.read("$[2]..[-2:-1]").toString() shouldBe """["ut",{"name":"Maryanne Wiggins","other":{"a":{"b":{"c":"yo"}}}}]""" - } - - it("should scan to keys on same level of all sublists") { - JsonPath.parse(LARGE_JSON)!!.read("$[2]..['name','id']").toString() shouldBe """["Marie Hampton","Felecia Bright",0,"Maryanne Wiggins","Marylou Caldwell",2]""" - } - - it("should scan to keys on different level of all sublists") { - JsonPath.parse(LARGE_JSON)!!.read("$[2]..['name','company','id']").toString() shouldBe """["Marie Hampton","ZENCO","Felecia Bright",0,"Maryanne Wiggins","Marylou Caldwell",2]""" - } - - it("should scan to get all items after the first of all sublists even if end out of range") { - JsonPath.parse(LARGE_JSON)!!.read("$[2]..[1:100]").toString() shouldBe """["elit","ipsum","pariatur","ullamco","ut","sint",{"name":"Maryanne Wiggins","other":{"a":{"b":{"c":"yo"}}}},{"id":2,"name":"Marylou Caldwell","other":{"a":{"b":{"c":"yo"}}}}]""" - } - - it("should get the key of every 2nd item in all sublists") { - val json = """ - { - "k": [{"key": "some value"}, {"key": 42}], - "kk": [[{"key": 100}, {"key": 200}, {"key": 300}], [{"key": 400}, {"key": 500}, {"key": 600}]], - "key": [0, 1] - } - """.trimIndent() - JsonPath.parse(json)!!.read("$..[1].key").toString() shouldBe """[42,200,500]""" - } - } - - describe("Wildcard") { - it("should handle all types of reads correctly") { - // we have obj, list, list, and scalars. It should handle them all correctly the further we go down with wildcards - // keep in mind wildcards always return a list, and drop scalars when going up a list. Scalars dont have levels to go up, only container nodes do - - JsonPath.parse("""[{"bar": [42]}]""")!!.read("$.*").toString() shouldBe """[{"bar":[42]}]""" // in root list - JsonPath.parse("""[{"bar": [42]}]""")!!.read("$.*.*").toString() shouldBe """[[42]]""" // root list wrapped around item from next level - JsonPath.parse("""[{"bar": [42]}]""")!!.read("$.*.*.*").toString() shouldBe """[42]""" // root list wrapped around item from next level - JsonPath.parse("""[{"bar": [42]}]""")!!.read("$.*.*.*.*").toString() shouldBe "[]" // root list since wildcard always returns a list - - JsonPath.parse("""[{"bar": [42]},{"bae": [30]}]""")!!.read("$.*").toString() shouldBe """[{"bar":[42]},{"bae":[30]}]""" // in root list - JsonPath.parse("""[{"bar": [42]},{"bae": [30]}]""")!!.read("$.*.*").toString() shouldBe """[[42],[30]]""" // root list wrapped around items from next level - JsonPath.parse("""[{"bar": [42]},{"bae": [30]}]""")!!.read("$.*.*.*").toString() shouldBe """[42,30]""" // root list wrapped around items from next level - JsonPath.parse("""[{"bar": [42]},{"bae": [30]}]""")!!.read("$.*.*.*.*").toString() shouldBe "[]" // root list since wildcard always returns a list - - JsonPath.parse("""[{"bar": [42]},{"bae": [30,31]}]""")!!.read("$.*").toString() shouldBe """[{"bar":[42]},{"bae":[30,31]}]""" // in root list - JsonPath.parse("""[{"bar": [42]},{"bae": [30,31]}]""")!!.read("$.*.*").toString() shouldBe """[[42],[30,31]]""" // root list wrapped around items from next level - JsonPath.parse("""[{"bar": [42]},{"bae": [30,31]}]""")!!.read("$.*.*.*").toString() shouldBe """[42,30,31]""" // root list wrapped around items from next level - JsonPath.parse("""[{"bar": [42]},{"bae": [30,31]}]""")!!.read("$.*.*.*.*").toString() shouldBe "[]" // root list since wildcard always returns a list - - JsonPath.parse("""[{"bar": [42], "scalarkey": "scalar2"}, "scalar1"]""")!!.read("$.*").toString() shouldBe """[{"bar":[42],"scalarkey":"scalar2"},"scalar1"]""" // in root list - JsonPath.parse("""[{"bar": [42], "scalarkey": "scalar2"}, "scalar1"]""")!!.read("$.*.*").toString() shouldBe """[[42],"scalar2"]""" // root list wrapped around items from next level - JsonPath.parse("""[{"bar": [42], "scalarkey": "scalar2"}, "scalar1"]""")!!.read("$.*.*.*").toString() shouldBe """[42]""" // root list wrapped around item from next level - JsonPath.parse("""[{"bar": [42], "scalarkey": "scalar2"}, "scalar1"]""")!!.read("$.*.*.*.*").toString() shouldBe "[]" // root list since wildcard always returns a list - - JsonPath.parse("""[[1], [2,3]]""")!!.read("$.*").toString() shouldBe """[[1],[2,3]]""" - JsonPath.parse("""[[1], [2,3]]""")!!.read("$.*.*").toString() shouldBe """[1,2,3]""" - JsonPath.parse("""[[1], [2,3]]""")!!.read("$.*.*.*").toString() shouldBe """[]""" - - JsonPath.parse("""[1,[2],[3,4],[5,6,7,[8,9,10,11]]]""")!!.read("$.*").toString() shouldBe """[1,[2],[3,4],[5,6,7,[8,9,10,11]]]""" - JsonPath.parse("""[1,[2],[3,4],[5,6,7,[8,9,10,11]]]""")!!.read("$.*.*").toString() shouldBe """[2,3,4,5,6,7,[8,9,10,11]]""" - JsonPath.parse("""[1,[2],[3,4],[5,6,7,[8,9,10,11]]]""")!!.read("$.*.*.*").toString() shouldBe """[8,9,10,11]""" - JsonPath.parse("""[1,[2],[3,4],[5,6,7,[8,9,10,11]]]""")!!.read("$.*.*.*.*").toString() shouldBe """[]""" - - JsonPath.parse("""[1,[2],[3,4],[5,6,7,[8,9,10,11]]]""")!!.read("$..*").toString() shouldBe """[1,[2],[3,4],[5,6,7,[8,9,10,11]],2,3,4,5,6,7,[8,9,10,11],8,9,10,11]""" - JsonPath.parse("""[1,[2],[3,4],[5,6,7,[8,9,10,11]]]""")!!.read("$..[*]").toString() shouldBe """[1,[2],[3,4],[5,6,7,[8,9,10,11]],2,3,4,5,6,7,[8,9,10,11],8,9,10,11]""" - JsonPath.parse("""[1,[2],[3,4],[5,6,7,[8,9,10,11]]]""")!!.read("$..*.*").toString() shouldBe """[2,3,4,5,6,7,[8,9,10,11],8,9,10,11]""" - JsonPath.parse("""[1,[2],[3,4],[5,6,7,[8,9,10,11]]]""")!!.read("$..*..*").toString() shouldBe """[2,3,4,5,6,7,[8,9,10,11],8,9,10,11,8,9,10,11]""" - } - - it("should handle lists properly") { - // Returns list held by "bar", wrapped in the root level list since wildcard always returns list - JsonPath.parse("""[{"bar": [42]}]""")!!.read("$[*].bar").toString() shouldBe """[[42]]""" - // Returns root level list now with the item from the inner list. - JsonPath.parse("""[{"bar": [42]}]""")!!.read("$.*.bar.*").toString() shouldBe """[42]""" - JsonPath.parse("""[{"bar": [42]}]""")!!.read("$[*].bar[*]").toString() shouldBe """[42]""" - // This would be a wildcard on a root level list which removes the scalars on that level. It should be list, not null as wildcard always returns list - JsonPath.parse("""[{"bar": [42]}]""")!!.read("$[*].bar[*][*]").toString() shouldBe """[]""" - - JsonPath.parse("""[[1], [2,3]]""")!!.read("$.*[0]").toString() shouldBe """[1,2]""" // first item of each sublist in root - JsonPath.parse("""[[1], [2,3]]""")!!.read("$.*[1]").toString() shouldBe """[3]""" // second item of each sublist in root, which there is only 1 of - JsonPath.parse("""[[1], [2,3]]""")!!.read("$.*[1].*").toString() shouldBe """[]""" // second item of each sublist in root, which there is only 1 of - - println(JsonPath.parse(LARGE_JSON)!!.read("$..friends").toString()) - println(JsonPath.parse(LARGE_JSON)!!.read("$..friends..name").toString()) - println(JsonPath.parse(LARGE_JSON)!!.read("$..friends..name[1]").toString()) - JsonPath.parse(LARGE_JSON)!!.read("$..friends.*.name").toString() shouldBe """["Kathrine Osborn","Vonda Howe","Harrell Pratt","Mason Leach","Spencer Valenzuela","Hope Medina","Felecia Bright","Maryanne Wiggins","Marylou Caldwell","Rios Norton","Judy Good","Rosetta Stanley","Lora Cotton","Gaines Henry","Dorothea Irwin"]""" - JsonPath.parse(LARGE_JSON)!!.read>("$..friends.[*].name") shouldBe listOf("Kathrine Osborn","Vonda Howe","Harrell Pratt","Mason Leach","Spencer Valenzuela","Hope Medina","Felecia Bright","Maryanne Wiggins","Marylou Caldwell","Rios Norton","Judy Good","Rosetta Stanley","Lora Cotton","Gaines Henry","Dorothea Irwin") - } - - it("should return null if null read before wildcard") { - JsonPath.parse("{}")!!.read("$.key.*") shouldBe null - JsonPath.parse("{}")!!.read("$.key[*]") shouldBe null - } - - it("should return self if used on scalar") { - JsonPath.parse("5")!!.read("$.*") shouldBe 5 - JsonPath.parse("5.34")!!.read("$.*") shouldBe 5.34 - JsonPath.parse("true")!!.read("$.*") shouldBe true - JsonPath.parse("false")!!.read("$.*") shouldBe false - JsonPath.parse(""""hello"""")!!.read("$.*") shouldBe "hello" - } - - it("should get the values of the JSON object") { - JsonPath.parse(LARGE_JSON)!!.read("$..friends[-1].*").toString() shouldBe """[2,"Harrell Pratt",{"a":{"b":{"c":"yo"}}},2,"Hope Medina",{"a":{"b":{"c":"yo"}}},2,"Marylou Caldwell",{"a":{"b":{"c":"yo"}}},2,"Rosetta Stanley",{"a":{"b":{"c":"yo"}}},2,"Dorothea Irwin",{"a":{"b":{"c":"yo"}}}]""" - JsonPath.parse(LARGE_JSON)!!.read("$..friends[-1][*]").toString() shouldBe """[2,"Harrell Pratt",{"a":{"b":{"c":"yo"}}},2,"Hope Medina",{"a":{"b":{"c":"yo"}}},2,"Marylou Caldwell",{"a":{"b":{"c":"yo"}}},2,"Rosetta Stanley",{"a":{"b":{"c":"yo"}}},2,"Dorothea Irwin",{"a":{"b":{"c":"yo"}}}]""" - } - } -}) diff --git a/src/test/kotlin/com/nfeld/jsonpathkt/PathCompilerTest.kt b/src/test/kotlin/com/nfeld/jsonpathkt/PathCompilerTest.kt deleted file mode 100644 index d44af76..0000000 --- a/src/test/kotlin/com/nfeld/jsonpathkt/PathCompilerTest.kt +++ /dev/null @@ -1,182 +0,0 @@ -package com.nfeld.jsonpathkt - -import com.nfeld.jsonpathkt.cache.CacheProvider -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.shouldBe -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.assertThrows - -class PathCompilerTest : StringSpec({ - - beforeTest { - CacheProvider.setCache(null) - } - - "compile" { - val f = PathCompiler::compile - - assertEquals(listOf( - ArrayAccessorToken(2), - DeepScanObjectAccessorToken(listOf("name","id"))), f("$[2]..['name','id']")) - assertEquals(listOf( - ArrayAccessorToken(2), - DeepScanObjectAccessorToken(listOf("name","id")), - ArrayAccessorToken(2)), f("$[2]..['name','id'][2]")) - - assertEquals(listOf(DeepScanObjectAccessorToken(listOf("name"))), f("$..['name']")) - assertEquals(listOf(DeepScanObjectAccessorToken(listOf("name","age"))), f("$..['name','age']")) - assertEquals(listOf(DeepScanArrayAccessorToken(listOf(0))), f("$..[0]")) - assertEquals(listOf(DeepScanArrayAccessorToken(listOf(0,1,6))), f("$..[0,1,6]")) - assertEquals(listOf(DeepScanArrayAccessorToken(listOf(0,-1,-6))), f("$..[0,-1,-6]")) - assertEquals(listOf(DeepScanArrayAccessorToken(listOf(-2))), f("$..[-2]")) - assertEquals(listOf(DeepScanArrayAccessorToken(listOf(0,1,2))), f("$..[0:3]")) - assertEquals(listOf(DeepScanArrayAccessorToken(listOf(0,1,2))), f("$..[:3]")) - assertEquals(listOf(DeepScanLengthBasedArrayAccessorToken(1, null, 0)), f("$..[1:]")) - assertEquals(listOf(DeepScanLengthBasedArrayAccessorToken(0, null, -2)), f("$..[:-2]")) - assertEquals(listOf(DeepScanLengthBasedArrayAccessorToken(-5, null, 0)), f("$..[-5:]")) - assertEquals(listOf(DeepScanLengthBasedArrayAccessorToken(0, null, 0)), f("$..[:]")) - assertEquals(listOf(DeepScanLengthBasedArrayAccessorToken(0, null, -2)), f("$..[0:-2]")) - assertEquals(listOf(DeepScanLengthBasedArrayAccessorToken(-5, 6, 0)), f("$..[-5:6]")) - assertEquals(listOf(DeepScanLengthBasedArrayAccessorToken(-5, null, -2)), f("$..[-5:-2]")) - assertEquals(listOf(ObjectAccessorToken("-")), f("$-")) - assertEquals(listOf(ObjectAccessorToken("-0")), f("$-0")) - assertEquals(listOf(ObjectAccessorToken("-"), ArrayAccessorToken(0)), f("$-[0]")) - assertEquals(listOf(WildcardToken()), f("$.*")) - assertEquals(listOf(WildcardToken(), ObjectAccessorToken("key")), f("$.*.key")) - assertEquals(listOf(WildcardToken(), ArrayAccessorToken(3)), f("$.*[3]")) - assertEquals(listOf(WildcardToken(), DeepScanObjectAccessorToken(listOf("key"))), f("$.*..key")) - assertEquals(listOf(WildcardToken(), DeepScanArrayAccessorToken(listOf(1,2,3))), f("$.*..[1:4]")) - f("""$..["key"]""") shouldBe listOf(DeepScanObjectAccessorToken(listOf("key"))) - f("$..*") shouldBe listOf(DeepScanWildcardToken()) - f("$..[*]") shouldBe listOf(DeepScanWildcardToken()) - f("$..*..*") shouldBe listOf(DeepScanWildcardToken(), DeepScanWildcardToken()) - f("$..[*]..[*]") shouldBe listOf(DeepScanWildcardToken(), DeepScanWildcardToken()) - } - - "should compile without root $ token" { - val f = PathCompiler::compile - - assertEquals(listOf(ObjectAccessorToken("key")), f("key")) - assertEquals(listOf(ObjectAccessorToken("key")), f("['key']")) - assertEquals(listOf(ObjectAccessorToken("key")), f("""["key"]""")) - assertEquals(listOf(ObjectAccessorToken("*")), f("*")) - assertEquals(listOf(ObjectAccessorToken("key"), ArrayAccessorToken(4)), f("key[4]")) - assertEquals(listOf(MultiObjectAccessorToken(listOf("a","b"))), f("['a','b']")) - assertEquals(listOf(MultiObjectAccessorToken(listOf("a","b"))), f("""["a","b"]""")) - assertEquals(listOf(ArrayAccessorToken(3)), f("[3]")) - assertEquals(listOf(MultiArrayAccessorToken(listOf(3,4))), f("[3,4]")) - assertEquals(listOf(MultiArrayAccessorToken(listOf(0,1,2))), f("[:3]")) - assertEquals(listOf(MultiArrayAccessorToken(listOf(0,1,2))), f("[0:3]")) - assertEquals(listOf(ArrayLengthBasedRangeAccessorToken(1, null, 0)), f("[1:]")) - } - - "should find matching closing bracket" { - val start = 0 - val f = PathCompiler::findMatchingClosingBracket - - assertEquals(1, f("[]", start)) - assertEquals(2, f("[5]", start)) - assertEquals(3, f("[53]", start)) - assertEquals(4, f("['5']", start)) - assertEquals(3, f("[-5]", start)) - assertEquals(4, f("[-5:]", start)) - assertEquals(3, f("[:5]", start)) - assertEquals(4, f("[0:5]", start)) - assertEquals(2, f("[:]", start)) - assertEquals(6, f("[0,1,2]", start)) - assertEquals(5, f("['a[']", start)) - assertEquals(5, f("['a]']", start)) - assertEquals(7, f("['a\\'b']", start)) - assertEquals(9, f("['a\\'\\']']", start)) - assertEquals(6, f("['4\\a']", start)) - assertEquals(7, f("""["a\"b"]""", start)) - assertEquals(9, f("""["a\"\"]"]""", start)) - assertEquals(6, f("""["4\a"]""", start)) - assertEquals(2, f("[*]", start)) - } - - "compileBracket" { - val f = PathCompiler::compileBracket - val start = 1 - var end = 0 - - fun findClosingIndex(path: String): String { - println("Testing $path") - end = PathCompiler.findMatchingClosingBracket(path, start) - return path - } - - assertEquals(ArrayAccessorToken(0), f(findClosingIndex("$[0]"), start, end)) - assertEquals(ArrayAccessorToken(-4), f(findClosingIndex("$[-4]"), start, end)) - assertEquals(MultiArrayAccessorToken(listOf(0,1,2)), f(findClosingIndex("$[:3]"), start, end)) - assertEquals(ArrayLengthBasedRangeAccessorToken(3, null,0), f(findClosingIndex("$[3:]"), start, end)) - assertEquals(ArrayLengthBasedRangeAccessorToken(0, null, 0), f(findClosingIndex("$[:]"), start, end)) - assertEquals(MultiArrayAccessorToken(listOf(1,2,3)), f(findClosingIndex("$[1:4]"), start, end)) - assertEquals(MultiArrayAccessorToken(listOf(1,2,3)), f(findClosingIndex("$[1,2,3]"), start, end)) - assertEquals(MultiArrayAccessorToken(listOf(1,-2,3)), f(findClosingIndex("$[1,-2,3]"), start, end)) - assertEquals(ObjectAccessorToken("name"), f(findClosingIndex("$['name']"), start, end)) - assertEquals(ObjectAccessorToken("4"), f(findClosingIndex("$['4']"), start, end)) - assertEquals(MultiObjectAccessorToken(listOf("name", "age")), f(findClosingIndex("$['name','age']"), start, end)) - assertEquals(MultiObjectAccessorToken(listOf("name", "age", "4")), f(findClosingIndex("$['name','age',4]"), start, end)) - assertEquals(ObjectAccessorToken("name:age"), f(findClosingIndex("$['name:age']"), start, end)) - assertEquals(WildcardToken(), f(findClosingIndex("$[*]"), start, end)) - assertEquals(ObjectAccessorToken(""":@."$,*'\"""), f(findClosingIndex("""$[':@."$,*\'\\']"""), start, end)) - assertEquals(ObjectAccessorToken(""), f(findClosingIndex("$['']"), start, end)) - assertEquals(ObjectAccessorToken(""), f(findClosingIndex("$[\"\"]"), start, end)) - assertEquals(ObjectAccessorToken("\\"), f(findClosingIndex("$['\\\\']"), start, end)) - assertEquals(ObjectAccessorToken("'"), f(findClosingIndex("$['\\'']"), start, end)) - assertEquals(ObjectAccessorToken("'"), f(findClosingIndex("$[\"'\"]"), start, end)) - assertEquals(ObjectAccessorToken("\""), f(findClosingIndex("$['\"']"), start, end)) - assertEquals(ObjectAccessorToken("\""), f(findClosingIndex("""$["\""]"""), start, end)) - - // handle negative values in array ranges - assertEquals(ArrayLengthBasedRangeAccessorToken(0,null, -1), f(findClosingIndex("$[:-1]"), start, end)) - assertEquals(ArrayLengthBasedRangeAccessorToken(0,null, -3), f(findClosingIndex("$[:-3]"), start, end)) - assertEquals(ArrayLengthBasedRangeAccessorToken(-1,null, 0), f(findClosingIndex("$[-1:]"), start, end)) - assertEquals(ArrayLengthBasedRangeAccessorToken(-5,null, 0), f(findClosingIndex("$[-5:]"), start, end)) - assertEquals(ArrayLengthBasedRangeAccessorToken(-5,null, -1), f(findClosingIndex("$[-5:-1]"), start, end)) - assertEquals(ArrayLengthBasedRangeAccessorToken(5,null, -1), f(findClosingIndex("$[5:-1]"), start, end)) - assertEquals(ArrayLengthBasedRangeAccessorToken(-5,4, 0), f(findClosingIndex("$[-5:4]"), start, end)) - - // ignore space paddings - assertEquals(ArrayAccessorToken(0), f(findClosingIndex("$[ 0 ]"), start, end)) - assertEquals(MultiArrayAccessorToken(listOf(0,3)), f(findClosingIndex("$[0, 3]"), start, end)) - assertEquals(ArrayLengthBasedRangeAccessorToken(0, null, 0), f(findClosingIndex("$[ : ]"), start, end)) - assertEquals(ArrayLengthBasedRangeAccessorToken(2, null, 0), f(findClosingIndex("$[ 2 : ]"), start, end)) - assertEquals(MultiArrayAccessorToken(listOf(0,1)), f(findClosingIndex("$[ : 2 ]"), start, end)) - assertEquals(MultiArrayAccessorToken(listOf(1,2)), f(findClosingIndex("$[ 1 : 3 ]"), start, end)) - assertEquals(WildcardToken(), f(findClosingIndex("$[ * ]"), start, end)) - assertEquals(ObjectAccessorToken("name"), f(findClosingIndex("$[ 'name' ]"), start, end)) - - // double quotes should be identical to single quotes - f(findClosingIndex("""$["key"]"""), start, end) shouldBe ObjectAccessorToken("key") - f(findClosingIndex("""$["'key'"]"""), start, end) shouldBe ObjectAccessorToken("'key'") - f(findClosingIndex("""$["ke'y"]"""), start, end) shouldBe ObjectAccessorToken("ke'y") - f(findClosingIndex("""$["ke\"y"]"""), start, end) shouldBe ObjectAccessorToken("ke\"y") - f(findClosingIndex("""$["key","key2"]"""), start, end) shouldBe MultiObjectAccessorToken(listOf("key", "key2")) - } - - "should throw" { - val compile = PathCompiler::compile - val compileBracket = PathCompiler::compileBracket - - assertThrows { compile("") } // path cannot be empty - assertThrows { compile("$[]") } // needs value in brackets - assertThrows { compile("$[") } // needs closing bracket - assertThrows { compile("$[[]") } // invalid char at end - assertThrows { compile("$[[]]") } - assertThrows { compile("$[[0]]") } - assertThrows { compile("$[0[0]]") } - assertThrows { compileBracket("$[]", 1, 2) } // no token returned - assertThrows { compile("$.") } // needs closing bracket - assertThrows { compile("$['\\") } // unexpected escape char - assertThrows { PathCompiler.findMatchingClosingBracket("$['4\\", 1) } - assertThrows { compile("$[-'0']") } // cant use both negative and object accessor - assertThrows { compile("$-[]") } - assertThrows { compile("$['single'quote']") } - assertThrows { compile("$[*,1]") } - assertThrows { compile("""$["'key"']""") } - assertThrows { compile("""$['"key'"]""") } - assertThrows { compile("""['a'.'b']""") } - } -}) diff --git a/src/test/kotlin/com/nfeld/jsonpathkt/PerfTest.kt b/src/test/kotlin/com/nfeld/jsonpathkt/PerfTest.kt deleted file mode 100644 index 75c8af5..0000000 --- a/src/test/kotlin/com/nfeld/jsonpathkt/PerfTest.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.nfeld.jsonpathkt - -import io.kotest.core.spec.style.StringSpec - -private const val DEFAULT_RUNS = 30 -private const val DEFAULT_CALLS_PER_RUN = 80000 -internal val timestamp: Long - get() = System.currentTimeMillis() - -private fun benchmark(callsPerRun: Int = DEFAULT_CALLS_PER_RUN, runs: Int = DEFAULT_RUNS, f: () -> Unit): Long { - // warmup - f() - - val times = mutableListOf() - - for (i in 0 until runs) { - val t1 = timestamp - for (k in 0 until callsPerRun) { - f() - } - val t2 = timestamp - times.add(t2 - t1) - } - - return times.average().toLong() -} - - -class PerfTest: StringSpec({ - val json = readTree(LARGE_JSON) - -}) diff --git a/src/test/kotlin/com/nfeld/jsonpathkt/TokenTest.kt b/src/test/kotlin/com/nfeld/jsonpathkt/TokenTest.kt deleted file mode 100644 index 5235dd2..0000000 --- a/src/test/kotlin/com/nfeld/jsonpathkt/TokenTest.kt +++ /dev/null @@ -1,381 +0,0 @@ -package com.nfeld.jsonpathkt - -import com.fasterxml.jackson.databind.node.ArrayNode -import com.fasterxml.jackson.databind.node.BooleanNode -import com.fasterxml.jackson.databind.node.TextNode -import com.nfeld.jsonpathkt.cache.CacheProvider -import com.nfeld.jsonpathkt.util.RootLevelArrayNode -import com.nfeld.jsonpathkt.util.createArrayNode -import com.nfeld.jsonpathkt.util.createObjectNode -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.shouldBe -import io.kotest.matchers.shouldNotBe -import org.junit.jupiter.api.Assertions.assertEquals - -private fun printTesting(subpath: String) { - println("Testing like $subpath") -} - -class TokenTest : DescribeSpec({ - beforeTest() { - CacheProvider.setCache(null) - } - - describe("Token tests") { - describe("ArrayAccessorToken") { - it("should be null if item doesnt exist at index") { - ArrayAccessorToken(0).read(createObjectNode()) shouldBe null - } - - it("should get the item if it exists at index") { - ArrayAccessorToken(0).read(readTree("[1,2]")).toString() shouldBe "1" - } - - it("should get the item if it exists at index if negative") { - ArrayAccessorToken(-1).read(readTree("[1,2]")).toString() shouldBe "2" - } - - it("should get last item") { - ArrayAccessorToken(-1).read(readTree("[1,2]")).toString() shouldBe "2" - } - - it("should be null if node is an ObjectNode") { - ArrayAccessorToken(0).read(readTree("""{"0":1}""")) shouldBe null - } - - it("should get item if node is a RootLevelArrayNode") { - val rootJson = readTree("[[1]]") as ArrayNode - ArrayAccessorToken(0).read(RootLevelArrayNode(rootJson)).toString() shouldBe "[1]" // list since it was root level - } - - it("should get first item of sublists if node is a RootLevelArrayNode") { - val rootJson = readTree("[1,[2],[3,4],[5,6,7]]") as ArrayNode - ArrayAccessorToken(0).read(RootLevelArrayNode(rootJson)).toString() shouldBe "[2,3,5]" - } - - it("should get last item of sublists if node is a RootLevelArrayNode") { - val rootJson = readTree("[1,[2],[3,4],[5,6,7]]") as ArrayNode - ArrayAccessorToken(-1).read(RootLevelArrayNode(rootJson)).toString() shouldBe "[2,4,7]" - } - - it("should get character of a String at specified index") { - ArrayAccessorToken(1).read(readTree("\"hello\"")).toString() shouldBe "\"e\"" - ArrayAccessorToken(-1).read(readTree("\"hello\"")).toString() shouldBe "\"o\"" - ArrayAccessorToken(-8).read(readTree("\"hello\"")) shouldBe null // out of bounds - } - - it("should get specified character of every String in RootLevelArrayNode") { - ArrayAccessorToken(1).read(WildcardToken().read(readTree("""["hello","world"]"""))).toString() shouldBe """["e","o"]""" - ArrayAccessorToken(-1).read(WildcardToken().read(readTree("""["hello","world"]"""))).toString() shouldBe """["o","d"]""" - ArrayAccessorToken(-4).read(WildcardToken().read(readTree("""["h","world"]"""))).toString() shouldBe """["o"]""" - } - } - - describe("MultiArrayAccessorToken") { - it("should get items at specified indices") { - MultiArrayAccessorToken(listOf(0, 1)).read(createObjectNode()).toString() shouldBe "[]" - - val expected = createArrayNode().apply { - add(1) - add(3) - } - assertEquals(expected.toString(), MultiArrayAccessorToken(listOf(0, -1)).read(createArrayNode().apply { - add(1) - add(2) - add(3) - }).toString()) - } - - it("should get specified items of sublists if node is a RootLevelArrayNode") { - val json = readTree("[1,[2],[3,4],[5,6,7]]") as ArrayNode - MultiArrayAccessorToken(listOf(0, 1)).read(RootLevelArrayNode(json)).toString() shouldBe "[2,3,4,5,6]" - MultiArrayAccessorToken(listOf(0, -1)).read(RootLevelArrayNode(json)).toString() shouldBe "[2,2,3,4,5,7]" - } - - it("should be able to get same index multiple times") { - val json = readTree("[1,[2],[3,4],[5,6,7]]") as ArrayNode - MultiArrayAccessorToken(listOf(0, 0, 0)).read(json).toString() shouldBe "[1,1,1]" - MultiArrayAccessorToken(listOf(2, 2)).read(json).toString() shouldBe "[[3,4],[3,4]]" - MultiArrayAccessorToken(listOf(0, 0)).read(RootLevelArrayNode(json)).toString() shouldBe "[2,2,3,3,5,5]" - } - - it("should get characters of a String at specified indices") { - MultiArrayAccessorToken(listOf(1,4)).read(readTree("\"hello\"")).toString() shouldBe """["e","o"]""" - } - - it("should get specified characters of every String in RootLevelArrayNode") { - MultiArrayAccessorToken(listOf(0,1)).read(WildcardToken().read(readTree("""["hello","world"]"""))).toString() shouldBe """["h","e","w","o"]""" - } - } - - describe("ArrayLengthBasedRangeAccessorToken") { - it("should return empty list") { - ArrayLengthBasedRangeAccessorToken(0).read(createObjectNode()).toString() shouldBe "[]" - } - - it("should not get characters of a String") { - ArrayLengthBasedRangeAccessorToken(1).read(readTree("\"hello\"")).toString() shouldBe "[]" - } - - it("should not get characters of every String in RootLevelArrayNode") { - ArrayLengthBasedRangeAccessorToken(0,2).read(WildcardToken().read(readTree("""["hello","world"]"""))).toString() shouldBe "[]" - ArrayLengthBasedRangeAccessorToken(2,null, -1).read(WildcardToken().read(readTree("""["hello","world"]"""))).toString() shouldBe "[]" - } - - it("should handle objects in RootLevelArrayNode") { - ArrayLengthBasedRangeAccessorToken(0, 1).read(WildcardToken().read(readTree("""[{"a":1,"b":{"c":2,"d":3},"e":4}]"""))).toString() shouldBe "[]" - ArrayLengthBasedRangeAccessorToken(0, -1).read(WildcardToken().read(readTree("""[{"a":1,"b":{"c":2,"d":3},"e":4}]"""))).toString() shouldBe "[]" - ArrayLengthBasedRangeAccessorToken(0, -1).read(WildcardToken().read(readTree("""[{"p":true},{"a":1,"b":{"c":2,"d":3},"e":4}]"""))).toString() shouldBe "[]" - } - - it("should handle different levels of list nesting") { - ArrayLengthBasedRangeAccessorToken(0, null, -1).read(readTree("""[1,[2],[3,4],[5,6,7]]""")).toString() shouldBe "[1,[2],[3,4]]" - ArrayLengthBasedRangeAccessorToken(0, null, 0).read(readTree("""[1,[2],[3,4],[5,6,7]]""")).toString() shouldBe "[1,[2],[3,4],[5,6,7]]" - ArrayLengthBasedRangeAccessorToken(0).read(WildcardToken().read(readTree("""[1,[2],[3,4],[5,6,7]]"""))).toString() shouldBe "[2,3,4,5,6,7]" - ArrayLengthBasedRangeAccessorToken(0, null, -1).read(WildcardToken().read(readTree("""[1,[2],[3,4],[5,6,7]]"""))).toString() shouldBe "[3,5,6]" - ArrayLengthBasedRangeAccessorToken(0, null, 0).read(WildcardToken().read(readTree("""[1,[2],[3,4],[5,6,7,[8,9,10,11]]]"""))).toString() shouldBe "[2,3,4,5,6,7,[8,9,10,11]]" - } - - it("to MultiArrayAccessorToken general cases") { - val json = readTree("[0,1,2,3,4]") as ArrayNode - - printTesting("[0:]") - var res = ArrayLengthBasedRangeAccessorToken(0, null, 0).toMultiArrayAccessorToken(json) - var expected = MultiArrayAccessorToken(listOf(0, 1, 2, 3, 4)) - assertEquals(expected, res) - - printTesting("[3:]") - res = ArrayLengthBasedRangeAccessorToken(3, null, 0).toMultiArrayAccessorToken(json) - expected = MultiArrayAccessorToken(listOf(3, 4)) - assertEquals(expected, res) - - printTesting("[:-1]") - res = ArrayLengthBasedRangeAccessorToken(0, null, -1).toMultiArrayAccessorToken(json) - expected = MultiArrayAccessorToken( - listOf( - 0, - 1, - 2, - 3 - ) - ) // this kind of range has end exclusive, so not really to end - assertEquals(expected, res) - - // test starting edge - printTesting("[:-4]") - res = ArrayLengthBasedRangeAccessorToken(0, null, -4).toMultiArrayAccessorToken(json) - expected = MultiArrayAccessorToken(listOf(0)) - assertEquals(expected, res) - - // test ending edge - printTesting("[-1:]") - res = ArrayLengthBasedRangeAccessorToken(-1, null, 0).toMultiArrayAccessorToken(json) - expected = MultiArrayAccessorToken(listOf(4)) - assertEquals(expected, res) - - printTesting("[-2:]") - res = ArrayLengthBasedRangeAccessorToken(-2, null, 0).toMultiArrayAccessorToken(json) - expected = MultiArrayAccessorToken(listOf(3, 4)) - assertEquals(expected, res) - - printTesting("[-4:-1]") - res = ArrayLengthBasedRangeAccessorToken(-4, null, -1).toMultiArrayAccessorToken(json) - expected = MultiArrayAccessorToken(listOf(1, 2, 3)) - assertEquals(expected, res) - - printTesting("[-4:4]") - res = ArrayLengthBasedRangeAccessorToken(-4, 4, 0).toMultiArrayAccessorToken(json) - expected = MultiArrayAccessorToken(listOf(1, 2, 3)) - assertEquals(expected, res) - - printTesting("[2:-1]") - res = ArrayLengthBasedRangeAccessorToken(2, null, -1).toMultiArrayAccessorToken(json) - expected = MultiArrayAccessorToken(listOf(2, 3)) - assertEquals(expected, res) - - printTesting("[:]") - res = ArrayLengthBasedRangeAccessorToken(0, null, 0).toMultiArrayAccessorToken(json) - expected = MultiArrayAccessorToken(listOf(0, 1, 2, 3, 4)) - assertEquals(expected, res) - } - } - - describe("DeepScanLengthBasedArrayAccessorToken") { - it("should handle general cases") { - val json = readTree("[0,1,2,3,4]") as ArrayNode - - printTesting("[0:]") - var res = DeepScanLengthBasedArrayAccessorToken(0, null, 0).read(json).toString() - assertEquals(json.toString(), res) - - printTesting("[1:]") - res = DeepScanLengthBasedArrayAccessorToken(1, null, 0).read(json).toString() - assertEquals("[1,2,3,4]", res) - - printTesting("[:-2]") - res = DeepScanLengthBasedArrayAccessorToken(0, null, -2).read(json).toString() - assertEquals("[0,1,2]", res) - - printTesting("[-3:]") - res = DeepScanLengthBasedArrayAccessorToken(-3, null, 0).read(json).toString() - assertEquals("[2,3,4]", res) - - printTesting("[0:-2]") - res = DeepScanLengthBasedArrayAccessorToken(0, null, -2).read(json).toString() - assertEquals("[0,1,2]", res) - - printTesting("[-4:3]") - res = DeepScanLengthBasedArrayAccessorToken(-4, 3, 0).read(json).toString() - assertEquals("[1,2]", res) - - printTesting("[-3:-1]") - res = DeepScanLengthBasedArrayAccessorToken(-3, null, -1).read(json).toString() - assertEquals("[2,3]", res) - } - - it("should handle different levels of list nesting") { - DeepScanLengthBasedArrayAccessorToken(0, null, 0).read(readTree("""[1,[2],[3,4],[5,6,7]]""")).toString() shouldBe "[1,[2],[3,4],[5,6,7],2,3,4,5,6,7]" - DeepScanLengthBasedArrayAccessorToken(0, null, -1).read(readTree("""[1,[2],[3,4],[5,6,7]]""")).toString() shouldBe "[1,[2],[3,4],3,5,6]" - DeepScanLengthBasedArrayAccessorToken(0, null, 0).read(WildcardToken().read(readTree("""[1,[2],[3,4],[5,6,7]]"""))).toString() shouldBe "[2,3,4,5,6,7]" - DeepScanLengthBasedArrayAccessorToken(0, null, -1).read(WildcardToken().read(readTree("""[1,[2],[3,4],[5,6,7]]"""))).toString() shouldBe "[3,5,6]" - DeepScanLengthBasedArrayAccessorToken(0, null, 0).read(WildcardToken().read(readTree("""[1,[2],[3,4],[5,6,7,[8,9,10,11]]]"""))).toString() shouldBe "[2,3,4,5,6,7,[8,9,10,11],8,9,10,11]" - } - } - - describe("ObjectAccessorToken") { - val objJson = readTree("""{"key":1}""") - val arrJson = readTree("""[{"key":1}]""") - // object accessor on an array should return list - - it("should get value from key if it exists") { - ObjectAccessorToken("key").read(objJson).toString() shouldBe "1" - } - - it("should be null if key doesnt exist") { - ObjectAccessorToken("missing").read(objJson) shouldBe null - } - - it("should be null if node is an ArrayNode") { - ObjectAccessorToken("key").read(arrJson) shouldBe null - } - - it("should get value from key if node is a RootLevelArrayNode") { - val rootJson = WildcardToken().read(arrJson) // should not be null - ObjectAccessorToken("key").read(rootJson).toString() shouldBe "[1]" // list since it was root level - } - } - - describe("MultiObjectAccessorToken") { - it("should return empty if accessing non root array or scalars") { - // object accessor on an array should consistently return list - MultiObjectAccessorToken(listOf("a", "b")).read(createArrayNode()).toString() shouldBe "[]" - MultiObjectAccessorToken(listOf("a", "b")).read(TextNode("yo")).toString() shouldBe "[]" - MultiObjectAccessorToken(listOf("a", "b")).read(BooleanNode.TRUE).toString() shouldBe "[]" - } - - it("should get the values if they exist in object") { - MultiObjectAccessorToken(listOf("a", "b")).read(readTree("""{"a":1,"b":2,"c":3}""")).toString() shouldBe "[1,2]" - MultiObjectAccessorToken(listOf("a", "b")).read(readTree("""{"a":1,"b":2}""")).toString() shouldBe "[1,2]" - MultiObjectAccessorToken(listOf("a", "b")).read(readTree("""{"a":1}""")).toString() shouldBe "[1]" - } - - it("should get the values from subcontainers in root array node") { - MultiObjectAccessorToken(listOf("a", "b")).read(RootLevelArrayNode(readTree("""[{"a":1,"b":2,"c":3}]""") as ArrayNode)).toString() shouldBe "[1,2]" - MultiObjectAccessorToken(listOf("a", "b")).read(RootLevelArrayNode(readTree("""[{"a":1,"b":2}]""") as ArrayNode)).toString() shouldBe "[1,2]" - MultiObjectAccessorToken(listOf("a", "b")).read(RootLevelArrayNode(readTree("""[{"a":1}]""") as ArrayNode)).toString() shouldBe "[1]" - MultiObjectAccessorToken(listOf("a", "b")).read(RootLevelArrayNode(readTree("""[{}]""") as ArrayNode)).toString() shouldBe "[]" - MultiObjectAccessorToken(listOf("a", "b")).read(RootLevelArrayNode(readTree("""[]""") as ArrayNode)).toString() shouldBe "[]" - } - } - - describe("DeepScanObjectAccessorToken") { - it("should scan for keys") { - DeepScanObjectAccessorToken(listOf("name")).read(readTree(FAMILY_JSON)).toString() shouldBe """["Thomas","Mila","Konstantin","Tracy"]""" - DeepScanObjectAccessorToken(listOf("nickname")).read(readTree(FAMILY_JSON)).toString() shouldBe """["Kons"]""" - DeepScanObjectAccessorToken(listOf("name","age")).read(readTree(FAMILY_JSON)).toString() shouldBe """["Thomas",13,"Mila",18,"Konstantin",29,"Tracy",4]""" - DeepScanObjectAccessorToken(listOf("name","nickname")).read(readTree(FAMILY_JSON)).toString() shouldBe """["Thomas","Mila","Konstantin","Kons","Tracy"]""" - } - - it("should place scan results into a RootLevelArrayNode") { - (DeepScanObjectAccessorToken(listOf("name")).read(readTree(FAMILY_JSON)) is RootLevelArrayNode) shouldBe true - } - - it("should handle objects on different levels") { - val json = readTree("""[{"a":1},{"a":2,"b":3},{"a":4,"b":5,"c":{"a":6,"b":7,"c":8}}]""") - DeepScanObjectAccessorToken(listOf("a")).read(json).toString() shouldBe """[1,2,4,6]""" - DeepScanObjectAccessorToken(listOf("c")).read(json).toString() shouldBe """[{"a":6,"b":7,"c":8},8]""" - DeepScanObjectAccessorToken(listOf("a","c")).read(json).toString() shouldBe """[1,2,4,{"a":6,"b":7,"c":8},6,8]""" - DeepScanObjectAccessorToken(listOf("a")).read(WildcardToken().read(json)).toString() shouldBe """[1,2,4,6]""" - DeepScanObjectAccessorToken(listOf("c")).read(WildcardToken().read(json)).toString() shouldBe """[{"a":6,"b":7,"c":8},8]""" - DeepScanObjectAccessorToken(listOf("a","c")).read(WildcardToken().read(json)).toString() shouldBe """[1,2,4,{"a":6,"b":7,"c":8},6,8]""" - } - } - - describe("DeepScanArrayAccessorToken") { - it("should scan for indices") { - DeepScanArrayAccessorToken(listOf(0)).read(readTree(FAMILY_JSON)).toString() shouldBe """[{"name":"Thomas","age":13}]""" - DeepScanArrayAccessorToken(listOf(0,2)).read(readTree(FAMILY_JSON)).toString() shouldBe """[{"name":"Thomas","age":13},{"name":"Konstantin","age":29,"nickname":"Kons"}]""" - } - - it("should place scan results into a RootLevelArrayNode") { - (DeepScanArrayAccessorToken(listOf(0)).read(readTree(FAMILY_JSON)) is RootLevelArrayNode) shouldBe true - } - - it("should handle different nested lists") { - val json = readTree("""[ {"a":1}, {"b":2}, [0,1,2, [ true, false ]] ]""") - DeepScanArrayAccessorToken(listOf(0)).read(json).toString() shouldBe """[{"a":1},0,true]""" - DeepScanArrayAccessorToken(listOf(0,1)).read(json).toString() shouldBe """[{"a":1},{"b":2},0,1,true,false]""" - - DeepScanArrayAccessorToken(listOf(0)).read(readTree("""[1,[2],[3,4],[5,6,7]]""")).toString() shouldBe "[1,2,3,5]" - DeepScanArrayAccessorToken(listOf(0, 1)).read(readTree("""[1,[2],[3,4],[5,6,7]]""")).toString() shouldBe "[1,[2],2,3,4,5,6]" - DeepScanArrayAccessorToken(listOf(0, -1)).read(readTree("""[1,[2],[3,4],[5,6,7]]""")).toString() shouldBe "[1,[5,6,7],2,2,3,4,5,7]" - DeepScanArrayAccessorToken(listOf(0)).read(WildcardToken().read(readTree("""[1,[2],[3,4],[5,6,7]]"""))).toString() shouldBe "[2,3,5]" - DeepScanArrayAccessorToken(listOf(0, 1)).read(WildcardToken().read(readTree("""[1,[2],[3,4],[5,6,7]]"""))).toString() shouldBe "[2,3,4,5,6]" - DeepScanArrayAccessorToken(listOf(0, -1)).read(WildcardToken().read(readTree("""[1,[2],[3,4],[5,6,7]]"""))).toString() shouldBe "[2,2,3,4,5,7]" - DeepScanArrayAccessorToken(listOf(0, 1)).read(WildcardToken().read(readTree("""[1,[2],[3,4],[5,6,7,[8,9,10,11]]]"""))).toString() shouldBe "[2,3,4,5,6,8,9]" - DeepScanArrayAccessorToken(listOf(0, -1)).read(WildcardToken().read(readTree("""[1,[2],[3,4],[5,6,7,[8,9,10,11]]]"""))).toString() shouldBe "[2,2,3,4,5,[8,9,10,11],8,11]" - } - } - - describe("WildcardToken") { - it("should handle empty cases") { - WildcardToken().read(createArrayNode()).toString() shouldBe """[]""" - WildcardToken().read(createObjectNode()).toString() shouldBe """[]""" - } - - it("should get values from objects and strip") { - val objectNode = readTree("""{ "some": "string", "int": 42, "object": { "key": "value" }, "array": [0, 1] }""") - WildcardToken().read(objectNode).toString() shouldBe """["string",42,{"key":"value"},[0,1]]""" - } - - it("should return a RootLevelArrayNode if root list replaced with another list before modifying values") { - val arrayNode = readTree("""["string", 42, { "key": "value" }, [0, 1] ]""") - WildcardToken().read(arrayNode).toString() shouldBe """["string",42,{"key":"value"},[0,1]]""" - } - - it("should drop scalars and move everything down on root RootLevelArrayNode") { - val arrayNode = readTree("""["string", 42, { "key": "value" }, [0, 1] ]""") - val res1 = WildcardToken().read(arrayNode) - (res1 is RootLevelArrayNode) shouldBe true - val res2 = WildcardToken().read(res1) - res2.toString() shouldBe """["value",0,1]""" - } - - it("should override toString, hashCode, and equals") { - WildcardToken().toString() shouldBe "WildcardToken" - WildcardToken().hashCode() shouldBe "WildcardToken".hashCode() - WildcardToken() shouldBe WildcardToken() - WildcardToken() shouldNotBe ArrayAccessorToken(0) - } - } - - describe("DeepScanWildcardToken") { - it("should override toString, hashCode, and equals") { - DeepScanWildcardToken().toString() shouldBe "DeepScanWildcardToken" - DeepScanWildcardToken().hashCode() shouldBe "DeepScanWildcardToken".hashCode() - DeepScanWildcardToken() shouldBe DeepScanWildcardToken() - DeepScanWildcardToken() shouldNotBe ArrayAccessorToken(0) - } - } - } -}) \ No newline at end of file diff --git a/src/test/kotlin/com/nfeld/jsonpathkt/cache/CacheProviderTest.kt b/src/test/kotlin/com/nfeld/jsonpathkt/cache/CacheProviderTest.kt deleted file mode 100644 index 143b22c..0000000 --- a/src/test/kotlin/com/nfeld/jsonpathkt/cache/CacheProviderTest.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.nfeld.jsonpathkt.cache - -import com.nfeld.jsonpathkt.JsonPath -import com.nfeld.jsonpathkt.resetCacheProvider -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.BeforeAll -import org.junit.jupiter.api.MethodOrderer -import org.junit.jupiter.api.Order -import org.junit.jupiter.api.TestMethodOrder - - -@TestMethodOrder(MethodOrderer.OrderAnnotation::class) -class CacheProviderTest { - - companion object { - @JvmStatic - @BeforeAll - fun resetCache() { - println("Setting up CacheProviderTest") - - resetCacheProvider() - } - } - - @Test - @Order(1) - fun shouldBeDefaultCache() { - val cache = CacheProvider.getCache() - assertTrue(cache is LRUCache) - } - - @Test - @Order(2) - fun shouldBeNoCache() { - CacheProvider.setCache(null) - assertNull(CacheProvider.getCache()) - } - - @Test - @Order(3) - fun shouldUseCustomCache() { - var calledGet = false - var calledPut = false - val cache = object : Cache { - override fun get(path: String): JsonPath? { - calledGet = true - return null - } - - override fun put(path: String, jsonPath: JsonPath) { - calledPut = true - } - } - CacheProvider.setCache(cache) - - assertFalse(calledGet) - assertFalse(calledPut) - - CacheProvider.getCache()?.get("") - assertTrue(calledGet) - assertFalse(calledPut) - - CacheProvider.getCache()?.put("", JsonPath("$")) - assertTrue(calledGet) - assertTrue(calledPut) - } -} \ No newline at end of file diff --git a/src/test/kotlin/com/nfeld/jsonpathkt/cache/CacheTest.kt b/src/test/kotlin/com/nfeld/jsonpathkt/cache/CacheTest.kt deleted file mode 100644 index 363b763..0000000 --- a/src/test/kotlin/com/nfeld/jsonpathkt/cache/CacheTest.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.nfeld.jsonpathkt.cache - -import com.nfeld.jsonpathkt.JsonPath -import com.nfeld.jsonpathkt.resetCacheProvider -import io.kotest.core.spec.style.StringSpec -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertNull -import org.junit.jupiter.api.Assertions.assertTrue - -private fun getCache(): Cache? = CacheProvider.getCache() -private fun getLruCache(): LRUCache? = getCache() as? LRUCache - -class CacheTest : StringSpec({ - "should get from cache" { - resetCacheProvider() - - val path = "$.some.path" - assertNull(getCache()!!.get(path)) - - val jsonPath = JsonPath(path) - getCache()!!.put(path, jsonPath) - assertEquals(jsonPath, getCache()!!.get(path)) - } - - "should put into cache" { - resetCacheProvider() - CacheProvider.maxCacheSize = 2 - assertEquals(2, CacheProvider.maxCacheSize) - - val path1 = "$.first" - val compiledPath1 = JsonPath(path1) - getCache()!!.put(path1, compiledPath1) - var cached = getLruCache()!!.toList() - assertTrue(cached.size == 1) - assertEquals(path1 to compiledPath1, cached[0]) - - val path2 = "$.second" - val compiledPath2 = JsonPath(path2) - getCache()!!.put(path2, compiledPath2) - cached = getLruCache()!!.toList() - assertTrue(cached.size == 2) - assertEquals(path1 to compiledPath1, cached[0]) - assertEquals(path2 to compiledPath2, cached[1]) - - // now lets test that most recently used order has changed by getting first path - val cachedResult = getCache()!!.get(path1) - cached = getLruCache()!!.toList() - assertEquals(compiledPath1.tokens, cachedResult?.tokens) - assertTrue(cached.size == 2) - assertEquals(path1 to compiledPath1, cached[1]) - assertEquals(path2 to compiledPath2, cached[0]) - - // with max cache size being 2, caching another path should remove least recently used, which is now path2 - val path3 = "$.third" - val compiledPath3 = JsonPath(path3) - getCache()!!.put(path3, compiledPath3) - cached = getLruCache()!!.toList() - assertTrue(cached.size == 2) // still needs to be 2 - assertEquals(path1 to compiledPath1, cached[0]) - assertEquals(path3 to compiledPath3, cached[1]) - } -}) \ No newline at end of file diff --git a/src/test/kotlin/com/nfeld/jsonpathkt/extension/JsonNodeTest.kt b/src/test/kotlin/com/nfeld/jsonpathkt/extension/JsonNodeTest.kt deleted file mode 100644 index 517b8de..0000000 --- a/src/test/kotlin/com/nfeld/jsonpathkt/extension/JsonNodeTest.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.nfeld.jsonpathkt.extension - -import com.nfeld.jsonpathkt.JsonPath -import com.nfeld.jsonpathkt.SMALL_JSON -import com.nfeld.jsonpathkt.SMALL_JSON_ARRAY -import com.nfeld.jsonpathkt.readTree -import com.nfeld.jsonpathkt.util.RootLevelArrayNode -import com.nfeld.jsonpathkt.util.createArrayNode -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.shouldBe -import io.kotest.matchers.shouldNotBe -import io.kotest.matchers.types.shouldBeSameInstanceAs -import io.kotest.matchers.types.shouldNotBeSameInstanceAs -import org.junit.jupiter.api.Assertions.assertEquals - -class JsonNodeTest : StringSpec({ - "should read root ArrayNode" { - val jsonObj = readTree(SMALL_JSON_ARRAY) - assertEquals(2, jsonObj.read(JsonPath("$[1]"))!!) - assertEquals(2, jsonObj.read("$[1]")!!) - } - - "should read root ObjectNode" { - val jsonObj = readTree(SMALL_JSON) - assertEquals(5, jsonObj.read(JsonPath("$['key']"))!!) - assertEquals(5, jsonObj.read("$['key']")!!) - } - - // we dont want to break any functionality if users expect to get a normal Jackson ArrayNode - "RootLevelArrayNode should be equal to an ArrayNode if every way since we return it to users" { - RootLevelArrayNode() shouldBe createArrayNode() - createArrayNode() shouldBe RootLevelArrayNode() - } -}) \ No newline at end of file diff --git a/src/test/kotlin/com/nfeld/jsonpathkt/util/JacksonUtilTest.kt b/src/test/kotlin/com/nfeld/jsonpathkt/util/JacksonUtilTest.kt deleted file mode 100644 index d797c08..0000000 --- a/src/test/kotlin/com/nfeld/jsonpathkt/util/JacksonUtilTest.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.nfeld.jsonpathkt.util - -import com.fasterxml.jackson.databind.node.ArrayNode -import com.fasterxml.jackson.databind.node.ObjectNode -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.shouldBe - -class JacksonUtilTest : StringSpec({ - "should create ObjectNode" { - createObjectNode() is ObjectNode - createObjectNode().toString() shouldBe "{}" - } - - "should create ArrayNode" { - createArrayNode() is ArrayNode - createArrayNode().toString() shouldBe "[]" - } -}) \ No newline at end of file