├── .github
└── workflows
│ ├── build-ios.yml
│ ├── build-jvm.yml
│ ├── publish-dry-run.yml
│ ├── publish-pages-only.yml
│ ├── publish.yml
│ ├── test-ios.yml
│ └── test-jvm.yml
├── .gitignore
├── CHANGELOG.md
├── CONTRIBUTING.md
├── DEVELOPMENT.md
├── LICENSE
├── README.md
├── build.gradle.kts
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── jsonpath4k.png
├── jsonpath4k
├── build.gradle.kts
└── src
│ ├── commonMain
│ ├── kotlin
│ │ └── at
│ │ │ └── asitplus
│ │ │ └── jsonpath
│ │ │ ├── JsonPath.kt
│ │ │ ├── JsonPathDependencyManager.kt
│ │ │ ├── JsonPathFunctionExtensionMapRepository.kt
│ │ │ ├── JsonPathFunctionExtensionRepository.kt
│ │ │ ├── core
│ │ │ ├── JsonPathCompiler.kt
│ │ │ ├── JsonPathExceptions.kt
│ │ │ ├── JsonPathFilterExpressionType.kt
│ │ │ ├── JsonPathFilterExpressionValue.kt
│ │ │ ├── JsonPathFunctionExtension.kt
│ │ │ ├── JsonPathQuery.kt
│ │ │ ├── JsonPathSelector.kt
│ │ │ ├── JsonPathSelectorQuery.kt
│ │ │ ├── NodeList.kt
│ │ │ ├── NormalizedJsonPath.kt
│ │ │ ├── NormalizedJsonPathSegment.kt
│ │ │ ├── Rfc8259Utils.kt
│ │ │ ├── Rfc9535Utils.kt
│ │ │ └── functionExtensions
│ │ │ │ ├── countFunctionExtension.kt
│ │ │ │ ├── lengthFunctionExtension.kt
│ │ │ │ ├── matchFunctionExtension.kt
│ │ │ │ ├── searchFunctionExtension.kt
│ │ │ │ └── valueFunctionExtension.kt
│ │ │ └── implementation
│ │ │ ├── AbstractSyntaxTree.kt
│ │ │ ├── AntlrJsonPathCompiler.kt
│ │ │ ├── AntlrJsonPathCompilerErrorListener.kt
│ │ │ ├── AntlrJsonPathCompilerException.kt
│ │ │ ├── AntlrJsonPathParserExtensions.kt
│ │ │ ├── AntlrJsonPathSemanticAnalyzerErrorListener.kt
│ │ │ ├── AntlrJsonPathSemanticAnalyzerVisitor.kt
│ │ │ ├── AntlrSyntaxErrorDetector.kt
│ │ │ ├── JsonPathExpression.kt
│ │ │ └── JsonPathExpressionEvaluationContext.kt
│ └── resources
│ │ └── grammar
│ │ ├── JsonPath.abnf
│ │ ├── JsonPathLexer.g4
│ │ └── JsonPathParser.g4
│ └── commonTest
│ └── kotlin
│ ├── KotestConfig.kt
│ └── at
│ └── asitplus
│ └── jsonpath
│ ├── DependencyManagementTest.kt
│ ├── JsonPathUnitTest.kt
│ ├── NodeListSerializationTest.kt
│ └── Rfc9535UtilsUnitTest.kt
└── settings.gradle.kts
/.github/workflows/build-ios.yml:
--------------------------------------------------------------------------------
1 | name: Build iOS Framework
2 | on: workflow_dispatch
3 | jobs:
4 | build:
5 | runs-on: macos-latest
6 | steps:
7 | - name: Checkout
8 | uses: actions/checkout@v3
9 | with:
10 | submodules: recursive
11 | - uses: actions/setup-java@v3
12 | with:
13 | distribution: 'temurin'
14 | java-version: '17'
15 | - name: Build klibs
16 | run: ./gradlew iosArm64MainKlibrary iosX64MainKlibrary
17 | - name: Build XCFrameworks
18 | run: ./gradlew assembleJsonPath4KXCFramework
19 | - name: Upload debug XCFramework jsonpath4k
20 | uses: actions/upload-artifact@v3
21 | with:
22 | name: JsonPath4K-debug.xcframework
23 | path: |
24 | jsonpath4k/build/XCFrameworks/debug/
25 | - name: Upload release XCFramework jsonpath4k
26 | uses: actions/upload-artifact@v3
27 | with:
28 | name: JsonPath4K-release.xcframework
29 | path: |
30 | jsonpath4k/build/XCFrameworks/release/
31 |
--------------------------------------------------------------------------------
/.github/workflows/build-jvm.yml:
--------------------------------------------------------------------------------
1 | name: Build JVM artifacts
2 | on: workflow_dispatch
3 | jobs:
4 | build:
5 | runs-on: ubuntu-latest
6 | steps:
7 | - name: Checkout
8 | uses: actions/checkout@v3
9 | with:
10 | submodules: recursive
11 | - uses: actions/setup-java@v3
12 | with:
13 | distribution: 'temurin'
14 | java-version: '17'
15 | - name: Build jar
16 | run: ./gradlew assemble
17 | - name: Upload jar jsonpath4k
18 | uses: actions/upload-artifact@v3
19 | with:
20 | name: jsonpath4k
21 | path: |
22 | jsonpath4k/build/libs/*jar
--------------------------------------------------------------------------------
/.github/workflows/publish-dry-run.yml:
--------------------------------------------------------------------------------
1 | name: Publish Dry Run
2 | on: workflow_dispatch
3 | permissions:
4 | contents: read
5 | pages: write
6 | id-token: write
7 | jobs:
8 | build:
9 | runs-on: macos-latest
10 | steps:
11 | - name: Checkout
12 | uses: actions/checkout@v3
13 | with:
14 | submodules: recursive
15 | - uses: actions/setup-java@v3
16 | with:
17 | distribution: 'temurin'
18 | java-version: '17'
19 | - name: Publish to Maven Local
20 | run: ./gradlew -Dpublishing.excludeIncludedBuilds=true clean publishToMavenLocal
21 | env:
22 | ORG_GRADLE_PROJECT_signingKeyId: ${{ secrets.PUBLISH_SIGNING_KEYID }}
23 | ORG_GRADLE_PROJECT_signingKey: ${{ secrets.PUBLISH_SIGNING_KEY }}
24 | ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.PUBLISH_SIGNING_PASSWORD }}
25 | ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.PUBLISH_SONATYPE_USER }}
26 | ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.PUBLISH_SONATYPE_PASSWORD }}
27 | deploy-docs:
28 | needs: build
29 | runs-on: ubuntu-latest
30 | steps:
31 | - name: Checkout
32 | uses: actions/checkout@v3
33 | with:
34 | submodules: recursive
35 | - name: Build Dokka HTML
36 | run: ./gradlew dokkaHtml
--------------------------------------------------------------------------------
/.github/workflows/publish-pages-only.yml:
--------------------------------------------------------------------------------
1 | name: Publish Pages
2 | on: workflow_dispatch
3 | permissions:
4 | contents: read
5 | pages: write
6 | id-token: write
7 | jobs:
8 | deploy-docs:
9 | environment:
10 | name: github-pages
11 | url: ${{ steps.deployment.outputs.page_url }}
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Checkout
15 | uses: actions/checkout@v3
16 | with:
17 | submodules: recursive
18 | - uses: actions/setup-java@v3
19 | with:
20 | distribution: 'temurin'
21 | java-version: '17'
22 | - name: Build Dokka HTML
23 | run: ./gradlew dokkaHtml
24 | - name: Setup Pages
25 | uses: actions/configure-pages@v3
26 | - name: Upload artifact
27 | uses: actions/upload-pages-artifact@v3
28 | with:
29 | # Upload docs folder
30 | path: './build/dokka'
31 | - name: Deploy to GitHub Pages
32 | id: deployment
33 | uses: actions/deploy-pages@v4
34 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish
2 | on: workflow_dispatch
3 | permissions:
4 | contents: read
5 | pages: write
6 | id-token: write
7 | jobs:
8 | build:
9 | runs-on: macos-latest
10 | steps:
11 | - name: Checkout
12 | uses: actions/checkout@v3
13 | with:
14 | submodules: recursive
15 | - uses: actions/setup-java@v3
16 | with:
17 | distribution: 'temurin'
18 | java-version: '17'
19 | - name: Publish to Sonatype
20 | run: ./gradlew -Dpublishing.excludeIncludedBuilds=true clean publishToSonatype closeSonatypeStagingRepository
21 | env:
22 | ORG_GRADLE_PROJECT_signingKeyId: ${{ secrets.PUBLISH_SIGNING_KEYID }}
23 | ORG_GRADLE_PROJECT_signingKey: ${{ secrets.PUBLISH_SIGNING_KEY }}
24 | ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.PUBLISH_SIGNING_PASSWORD }}
25 | ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.PUBLISH_SONATYPE_USER }}
26 | ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.PUBLISH_SONATYPE_PASSWORD }}
27 | deploy-docs:
28 | needs: build
29 | environment:
30 | name: github-pages
31 | url: ${{ steps.deployment.outputs.page_url }}
32 | runs-on: ubuntu-latest
33 | steps:
34 | - name: Checkout
35 | uses: actions/checkout@v3
36 | with:
37 | submodules: recursive
38 | - name: Build Dokka HTML
39 | run: ./gradlew dokkaHtml
40 | - name: Setup Pages
41 | uses: actions/configure-pages@v3
42 | - name: Upload artifact
43 | uses: actions/upload-pages-artifact@v3
44 | with:
45 | # Upload docs folder
46 | path: './build/dokka'
47 | - name: Deploy to GitHub Pages
48 | id: deployment
49 | uses: actions/deploy-pages@v4
50 |
--------------------------------------------------------------------------------
/.github/workflows/test-ios.yml:
--------------------------------------------------------------------------------
1 | name: Test iOS implementation
2 | on: [push]
3 | jobs:
4 | build:
5 | runs-on: macos-latest
6 | steps:
7 | - name: Checkout
8 | uses: actions/checkout@v3
9 | with:
10 | submodules: recursive
11 | - uses: actions/setup-java@v3
12 | with:
13 | distribution: 'temurin'
14 | java-version: '17'
15 | - name: Build klibs
16 | run: ./gradlew iosArm64MainKlibrary iosX64MainKlibrary
17 | - name: Run tests
18 | run: ./gradlew iosSimulatorArm64Test
19 | - name: Test Report
20 | uses: dorny/test-reporter@v1
21 | if: success() || failure()
22 | with:
23 | name: jsonpath4k Tests
24 | path: jsonpath4k/build/test-results/**/TEST*.xml
25 | reporter: java-junit
26 |
--------------------------------------------------------------------------------
/.github/workflows/test-jvm.yml:
--------------------------------------------------------------------------------
1 | name: Test JVM implementation
2 | on: [push]
3 | jobs:
4 | build:
5 | runs-on: ubuntu-latest
6 | steps:
7 | - name: Checkout
8 | uses: actions/checkout@v3
9 | with:
10 | submodules: recursive
11 | - uses: actions/setup-java@v3
12 | with:
13 | distribution: 'temurin'
14 | java-version: '17'
15 | - name: Run tests
16 | run: ./gradlew jvmTest
17 | - name: Test Report
18 | uses: dorny/test-reporter@v1
19 | if: success() || failure()
20 | with:
21 | name: jsonpath4k Tests
22 | path: jsonpath4k/build/test-results/**/TEST*.xml
23 | reporter: java-junit
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | jsonpath4k/src/gen
2 | .kotlin
3 | *.iml
4 | .gradle
5 | **/build/
6 | xcuserdata
7 | !src/**/build/
8 | local.properties
9 | .idea
10 | .DS_Store
11 | captures
12 | .externalNativeBuild
13 | .cxx
14 | *.xcodeproj/*
15 | !*.xcodeproj/project.pbxproj
16 | !*.xcodeproj/xcshareddata/
17 | !*.xcodeproj/project.xcworkspace/
18 | !*.xcworkspace/contents.xcworkspacedata
19 | **/xcshareddata/WorkspaceSettings.xcsettings
20 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Release NEXT
2 |
3 | # Release 2.4.1
4 | * Introduce dedicated Android artefact targeting JDK 1.8
5 | * Kotlin 2.1.0
6 |
7 | # Release 2.4.0
8 | * ANTLR 1.0.0 Stable
9 | * Kotest Snapshot to get iOS tests working again
10 | * Rename token and expression names to avoid collisions with keywords
11 |
12 | # Release 2.3.0:
13 | * Kotlin 2.0.20 (binary-incompatible change, makes iOS Tests fail too)
14 | * Serialization 1.7.2
15 | * Antlr-Kotlin 1.0.0-RC5
16 | * add tests
17 | * auto-generate Kotlin sources from Antlr-files on every Gradle invocation
18 |
19 | # Release 2.2.0:
20 | - Rebranding to JsonPath4K
21 | - change Maven coordinates to `at.asitplus:jsonpath4k`
22 | - publish relocation POM for Version 2.0.0
23 | - Dependency Updates
24 | - Update to Kotlin 2.0.0
25 | - Update to Kotest 5.9.1
26 | - Update to kotlinx-serialization 1.7.1
27 | - Gradle 8.9
28 | - Antlr-Kotlin 1.0.0-RC4
29 |
30 | # Release 2.1.0:
31 | - Add: Serialization for `NormalizedJsonPathSegment`, `NormalizedJsonPath` and `NodeListEntry`
32 |
33 | # Release 2.0.0:
34 | - BREAKING CHANGE to `JsonPathFunctionExtension`: breaks specification syntax for function extensions, but provides simpler definition syntax.
35 | - The function extensions no longer hold a name. A name must only be provided when adding a function extension to a repository
36 | - The function extension classes are no longer inheritable. Instances must be created from constructors.
37 | - BREAKING CHANGE to `JsonPathFunctionExtensionRepository`:
38 | - Changed `addExtension`:
39 | - Takes an additional parameter for the function extension name
40 | - The function extension is now constructed only when necessary, which has been changed mostly because the definition syntax now feels cleaner.
41 | - BREAKING CHANGE to `JsonPathCompiler`:
42 | - Changed `compile`: Takes the function extension retriever as second argument now
43 |
44 | # Release 1.0.0:
45 | - Add `JsonPath`: JsonPath compiler and query functionality
46 | - Add `JsonPathDependencyManager`: Dependency manager for the library
47 | - Add `JsonPathFunctionExtensionRepository`: Give users a way to add custom function extensions
48 |
49 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
4 | # Contributing to A-SIT Plus Open Source
5 |
6 |
7 |
8 | We are happy to receive external contributions!
9 | "Just" opening issues for encountered problems is greatly appreciated too!
10 |
11 | ## Reporting Issues
12 |
13 | We welcome constructive feedback and enquiries of any kind! Just be sure to check existing issues first to avoid duplicates.
14 | Please try to be as precise as possible and provide a reproducer (where applicable).
15 |
16 | ## Contributing Changes
17 |
18 | We expect the contributor to hold all rights to the contributions they are about to commit.
19 | We particularly condemn copyright infringement and expect contributors to respect this position and strictly observe applicable law!
20 |
21 | If you plan on contributing changes to this repository's contents, please
22 |
23 | 1. Fork it
24 | 2. Create a branch with a descriptive name (e.g. `feature/timeTravel` or `fix/gravitationalConstant`)
25 | 3. Commit your changes to your branch
26 | 4. Open a pull request
27 |
28 | We will then review the changes, provide feedback.
29 | Once we agree that the PR is ready, we will approve it, and your PR will be merged.
30 |
31 | This project **does not** require external contributors to sign a Contributor License Agreement (CLA)!
32 | Contributions are only subject to the terms of this project's license.
33 |
34 | ### Coding Conventions
35 |
36 | We try to follow the [official Kotlin coding conventions](https://kotlinlang.org/docs/coding-conventions.html) and expect the same from external contributors.
37 | We don't have any automated checks in place and hence don't strictly enforce convention rules by hard, but we will manually check for obvious violations.
38 | To put it plain and simple:
39 |
40 | > When in Rome, do as the Romans do!
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | Copyright © 2024 A-SIT Plus GmbH, Seidlgasse 22 / Top 9, 1030 Vienna, Austria
49 |
50 |

51 |
52 |
53 |
--------------------------------------------------------------------------------
/DEVELOPMENT.md:
--------------------------------------------------------------------------------
1 | # JsonPath Library
2 |
3 | ## Development
4 |
5 | Development happens in branch development. The main branch always tracks the latest release. Hence, create PRs against development. Use dedicated release/x.y.z branches to prepare releases and create release PRs against main, which will then be merged back into development.
6 |
7 | ## Publishing
8 |
9 | Create a GPG key with `gpg --gen-key`, and export it with `gpg --keyring secring.gpg --export-secret-keys > ~/.gnupg/secring.gpg`. Be sure to publish it with `gpg --keyserver keyserver.ubuntu.com --send-keys `. See also the information in the [Gradle docs](https://docs.gradle.org/current/userguide/signing_plugin.html).
10 |
11 | Create a user token for your Nexus account on (in your profile) to use as `sonatypeUsername` and `sonatypePassword`.
12 |
13 | Configure your `~/.gradle/gradle.properties`:
14 |
15 | ```properties
16 | signing.keyId=
17 | signing.password=
18 | signing.secretKeyRingFile=
19 | sonatypeUsername=
20 | sonatypePassword=
21 | ```
22 |
23 | In addition, it is highly recommended to set the System property `publishing.excludeIncludedBuilds` to `true`, to
24 | build artefacts for publishing, which **do no** depend on included builds.
25 |
26 | To run the pipeline from GitHub, export your GPG key with `gpg --export-secret-keys --armor | tee .asc` and set the following environment variables:
27 |
28 | ```shell
29 | ORG_GRADLE_PROJECT_signingKeyId=
30 | ORG_GRADLE_PROJECT_signingKey=
31 | ORG_GRADLE_PROJECT_signingPassword=
32 | ORG_GRADLE_PROJECT_sonatypeUsername=
33 | ORG_GRADLE_PROJECT_sonatypePassword=
34 | ```
35 |
36 | Actually, these environment variables are read from the repository secrets configured on Github.
37 |
38 | Publish with:
39 |
40 | ```shell
41 | ./gradlew clean publishToSonatype
42 | ```
43 |
44 | To also release the artifacts to Maven Central run:
45 |
46 | ```shell
47 | ./gradlew clean publishToSonatype closeAndReleaseSonatypeStagingRepository
48 | ```
49 |
50 | To publish locally for testing, one can skip the signing tasks:
51 |
52 | ```shell
53 | ./gradlew clean publishToMavenLocal -x signJvmPublication -x signKotlinMultiplatformPublication -x signIosArm64Publication -x signIosSimulatorArm64Publication -x signIosX64Publication
54 | ```
55 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # 
4 |
5 |
6 |
7 |
8 | [](https://a-sit-plus.github.io)
9 | [](http://www.apache.org/licenses/LICENSE-2.0)
10 | [](http://kotlinlang.org)
11 | [](http://kotlinlang.org)
12 | [](https://www.oracle.com/java/technologies/downloads/#java17)
13 | [](https://mvnrepository.com/artifact/at.asitplus/jsonpath4k/)
14 |
15 | This is a Kotlin Multiplatform Library for using Json Paths as specified in [RFC9535](https://datatracker.ietf.org/doc/rfc9535).
16 |
17 | ## Architecture
18 |
19 | This library was built for [Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.html) targeting JVM/Android and iOS. Other targets might work, but are
20 | not tested or even built.
21 |
22 | Notable features for multiplatform are:
23 |
24 | - Use of [Napier](https://github.com/AAkira/Napier) as the logging framework for the default compiler instance
25 | - Use of [Kotest](https://kotest.io/) for unit tests
26 | - Use of [kotlinx-serialization](https://github.com/Kotlin/kotlinx.serialization) for serialization from/to JSON and to have JsonElement as evaluation target for JsonPathQuery
27 |
28 | ## Using the Library
29 | 1. Add JsonPath4K as a dependency in your project (`at.situplus:jsonpath4k:$version`)
30 | 2. Use the `JsonPath` constructor for compiling JSONPath query expressions.
31 | 3. Invoke the method `JsonPath.query` to select nodes satisfying the JsonPath query expression from a `JsonElement`.
32 | 4. A `nodeList` containing both the selected values and their normalized paths is returned.
33 |
34 | In general, things are called what they are. Most prominently, _JsonPath4K_ is not used anywhere in code – even though this
35 | project is called JsonPath4K, it provides _JSONPath_ functionality for Kotlin Multiplatform,
36 | not _JsonPath4K_ functionality because there is no such thing.
37 |
38 | ```kotlin
39 | val jsonElement = buildJsonArray { add(0) }
40 |
41 | val jsonPathQueryExpression = "$[0]"
42 | val jsonPath = JsonPath(jsonPathQueryExpression)
43 |
44 | val nodeList = jsonPath.query(jsonElement)
45 | val jsonValue = nodeList[0].value.jsonPrimitive
46 | val normalizedPath = nodeList[0].normalizedJsonPath
47 | ```
48 |
49 | ## Function extensions
50 | This library supports the function extensions specified in [RFC9535](https://www.rfc-editor.org/rfc/rfc9535.html#name-function-extensions) by default.
51 |
52 | ### Custom function extensions
53 | Custom function extensions can be added using `JsonPathDependencyManager.functionExtensionRepository.addExtension`:
54 | ```kotlin
55 | // adding a logical type function extension with 1 parameter of type NodesType
56 | JsonPathDependencyManager.functionExtensionRepository.addExtension("foo") {
57 | JsonPathFunctionExtension.LogicalTypeFunctionExtension(
58 | JsonPathFilterExpressionType.NodesType
59 | ) {
60 | true
61 | }
62 | }
63 |
64 | // adding a value type function extension returning a JsonValue with 2 parameters of type ValueType
65 | JsonPathDependencyManager.functionExtensionRepository.addExtension("foo") {
66 | JsonPathFunctionExtension.ValueTypeFunctionExtension(
67 | JsonPathFilterExpressionType.ValueType,
68 | JsonPathFilterExpressionType.ValueType,
69 | ) {
70 | JsonPrimitive("")
71 | }
72 | }
73 |
74 | // adding a value type function extension returning the JsonValue with 2 parameters of type ValueType
75 | JsonPathDependencyManager.functionExtensionRepository.addExtension("foo") {
76 | JsonPathFunctionExtension.ValueTypeFunctionExtension(
77 | JsonPathFilterExpressionType.ValueType,
78 | JsonPathFilterExpressionType.ValueType,
79 | ) {
80 | JsonNull
81 | }
82 | }
83 |
84 | // adding a value type function extension returning the special value `Nothing` with 2 parameters of type LogicalType
85 | JsonPathDependencyManager.functionExtensionRepository.addExtension("foo") {
86 | JsonPathFunctionExtension.ValueTypeFunctionExtension(
87 | JsonPathFilterExpressionType.LogicalType,
88 | JsonPathFilterExpressionType.LogicalType,
89 | ) {
90 | null
91 | }
92 | }
93 |
94 | // adding a nodes type function extension with 2 parameters of type ValueType
95 | JsonPathDependencyManager.functionExtensionRepository.addExtension("foo") {
96 | JsonPathFunctionExtension.NodesTypeFunctionExtension(
97 | JsonPathFilterExpressionType.ValueType,
98 | JsonPathFilterExpressionType.ValueType,
99 | ) {
100 | listOf()
101 | }
102 | }
103 |
104 | // adding a nodes type function extension with 2 parameters of type ValueType
105 | JsonPathDependencyManager.functionExtensionRepository.addExtension("foo") {
106 | JsonPathFunctionExtension.NodesTypeFunctionExtension(
107 | JsonPathFilterExpressionType.ValueType,
108 | JsonPathFilterExpressionType.ValueType,
109 | ) {
110 | listOf()
111 | }
112 | }
113 | ```
114 |
115 | ### Removing Function extensions
116 | Function extensions can be removed from the default repository by setting the value of `JsonPathDependencyManager.functionExtensionRepository` to a new repository.
117 |
118 | Existing functions can be preserved by exporting them using `JsonPathDependencyManager.functionExtensionRepository.export()` and selectively importing them into the new repository.
119 |
120 |
121 |
122 | ### Testing custom function extensions
123 | In order to test custom function extensions without polluting the default function extension repository, it is advised to make an export and use the resulting map to build a function extension retriever.
124 |
125 | ```kotlin
126 | val testRetriever = JsonPathDependencyManager.functionExtensionRepository.export().plus(
127 | "foo" to JsonPathFunctionExtension.LogicalTypeFunctionExtension(
128 | JsonPathFilterExpressionType.ValueType,
129 | JsonPathFilterExpressionType.ValueType,
130 | ) {
131 | true
132 | }
133 | )
134 | val jsonPath = JsonPath(jsonPathStatement, functionExtensionRetriever = testRetriever::get)
135 |
136 | // select from a json element
137 | jsonPath.query(buildJsonElement {})
138 | ```
139 |
140 | ## Error handeling
141 | The default compiler uses Napier for reporting errors.
142 | It is possible to implement a custom error listener by extending `AntlrJsonPathCompilerErrorListener` and setting a new default compiler:
143 | ```kotlin
144 | JsonPathDependencyManager.compiler = AntlrJsonPathCompiler(
145 | errorListener = object : AntlrJsonPathCompilerErrorListener {
146 | //TODO: IMPLEMENT MEMBERS
147 | },
148 | )
149 | ```
150 |
151 | ## Contributing
152 | External contributions are greatly appreciated!
153 | Just be sure to observe the contribution guidelines (see [CONTRIBUTING.md](CONTRIBUTING.md)).
154 |
155 |
156 |
157 |
158 | ---
159 |
160 | The Apache License does not apply to the logos, (including the A-SIT logo) and the project/module name(s), as these are the sole property of
161 | A-SIT/A-SIT Plus GmbH and may not be used in derivative works without explicit permission!
162 |
163 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | // this is necessary to avoid the plugins to be loaded multiple times
3 | // in each subproject's classloader
4 | alias(libs.plugins.gradle.nexus.publish)
5 | alias(libs.plugins.android.library) apply (false)
6 | }
7 |
8 | repositories {
9 | maven("https://maven.pkg.jetbrains.space/kotlin/p/dokka/dev")
10 | mavenCentral()
11 | google()
12 | gradlePluginPortal()
13 | }
14 | nexusPublishing {
15 | nexusPublishing {
16 | repositories {
17 | sonatype { //only for users registered in Sonatype after 24 Feb 2021
18 | nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/"))
19 | snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/"))
20 | }
21 | }
22 | }
23 | }
24 |
25 | val artifactVersion: String by extra
26 | group = "at.asitplus"
27 | version = artifactVersion
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | #Gradle
2 | org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M"
3 |
4 | #Kotlin
5 | kotlin.code.style=official
6 |
7 | #Android
8 | android.useAndroidX=true
9 |
10 | #MPP
11 | kotlin.mpp.enableCInteropCommonization=true
12 | kotlin.mpp.stability.nowarn=true
13 | kotlin.native.ignoreDisabledTargets=true
14 |
15 |
16 | # workaround dokka bug (need to wait for next snapshot build)
17 | org.jetbrains.dokka.classpath.excludePlatformDependencyFiles=true
18 |
19 | artifactVersion = 2.4.1
20 | jdk.version=17
21 |
22 | android.experimental.lint.version=8.5.0
23 | android.lint.useK2Uast=true
24 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | antlr-kotlin = "1.0.0"
3 | kotest = "5.9.1"
4 | kotest-plugin = "6.0.0.M1"
5 | jetbrains-kotlin = "2.1.0"
6 | jetbrains-kotlinx-serialization = "1.8.0"
7 | jetbrains-dokka = "1.9.20"
8 | napier = "2.7.1"
9 | gradle-nexus-publish = "2.0.0"
10 | agp = "8.2.2"
11 |
12 | [libraries]
13 | antlr-kotlin = { group = "com.strumenta", name = "antlr-kotlin-runtime", version.ref = "antlr-kotlin" }
14 | napier = { module = "io.github.aakira:napier", version.ref = "napier" }
15 |
16 | kotest-common = { module = "io.kotest:kotest-common", version.ref = "kotest" }
17 | kotest-property = { module = "io.kotest:kotest-property", version.ref = "kotest" }
18 | kotest-assertions-core = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" }
19 | kotest-framework-engine = { module = "io.kotest:kotest-framework-engine", version.ref = "kotest" }
20 | kotest-framework-datatest = { module = "io.kotest:kotest-framework-datatest", version.ref = "kotest" }
21 | kotest-runner-junit5 = { module = "io.kotest:kotest-runner-junit5", version.ref = "kotest" }
22 |
23 | jetbrains-kotlinx-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "jetbrains-kotlinx-serialization" }
24 |
25 |
26 | [plugins]
27 | antlr-kotlin-plugin = { id = "com.strumenta.antlr-kotlin", version.ref = "antlr-kotlin" }
28 | kotest-multiplatform = { id = "io.kotest.multiplatform", version.ref = "kotest-plugin" }
29 | jetbrains-kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "jetbrains-kotlin" }
30 | jetbrains-kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "jetbrains-kotlin" }
31 | jetbrains-dokka = { id = "org.jetbrains.dokka", version.ref = "jetbrains-dokka" }
32 | gradle-nexus-publish = { id = "io.github.gradle-nexus.publish-plugin", version.ref = "gradle-nexus-publish" }
33 | android-library = {id = "com.android.library", version.ref = "agp"}
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/a-sit-plus/jsonpath4k/b72fb2e6179b535820be76d8d1280a60761b455f/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | # This is normally unused
84 | # shellcheck disable=SC2034
85 | APP_BASE_NAME=${0##*/}
86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
87 |
88 | # Use the maximum available, or set MAX_FD != -1 to use that value.
89 | MAX_FD=maximum
90 |
91 | warn () {
92 | echo "$*"
93 | } >&2
94 |
95 | die () {
96 | echo
97 | echo "$*"
98 | echo
99 | exit 1
100 | } >&2
101 |
102 | # OS specific support (must be 'true' or 'false').
103 | cygwin=false
104 | msys=false
105 | darwin=false
106 | nonstop=false
107 | case "$( uname )" in #(
108 | CYGWIN* ) cygwin=true ;; #(
109 | Darwin* ) darwin=true ;; #(
110 | MSYS* | MINGW* ) msys=true ;; #(
111 | NONSTOP* ) nonstop=true ;;
112 | esac
113 |
114 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
115 |
116 |
117 | # Determine the Java command to use to start the JVM.
118 | if [ -n "$JAVA_HOME" ] ; then
119 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
120 | # IBM's JDK on AIX uses strange locations for the executables
121 | JAVACMD=$JAVA_HOME/jre/sh/java
122 | else
123 | JAVACMD=$JAVA_HOME/bin/java
124 | fi
125 | if [ ! -x "$JAVACMD" ] ; then
126 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
127 |
128 | Please set the JAVA_HOME variable in your environment to match the
129 | location of your Java installation."
130 | fi
131 | else
132 | JAVACMD=java
133 | if ! command -v java >/dev/null 2>&1
134 | then
135 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
136 |
137 | Please set the JAVA_HOME variable in your environment to match the
138 | location of your Java installation."
139 | fi
140 | fi
141 |
142 | # Increase the maximum file descriptors if we can.
143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
144 | case $MAX_FD in #(
145 | max*)
146 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
147 | # shellcheck disable=SC3045
148 | MAX_FD=$( ulimit -H -n ) ||
149 | warn "Could not query maximum file descriptor limit"
150 | esac
151 | case $MAX_FD in #(
152 | '' | soft) :;; #(
153 | *)
154 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
155 | # shellcheck disable=SC3045
156 | ulimit -n "$MAX_FD" ||
157 | warn "Could not set maximum file descriptor limit to $MAX_FD"
158 | esac
159 | fi
160 |
161 | # Collect all arguments for the java command, stacking in reverse order:
162 | # * args from the command line
163 | # * the main class name
164 | # * -classpath
165 | # * -D...appname settings
166 | # * --module-path (only if needed)
167 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
168 |
169 | # For Cygwin or MSYS, switch paths to Windows format before running java
170 | if "$cygwin" || "$msys" ; then
171 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
172 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
173 |
174 | JAVACMD=$( cygpath --unix "$JAVACMD" )
175 |
176 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
177 | for arg do
178 | if
179 | case $arg in #(
180 | -*) false ;; # don't mess with options #(
181 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
182 | [ -e "$t" ] ;; #(
183 | *) false ;;
184 | esac
185 | then
186 | arg=$( cygpath --path --ignore --mixed "$arg" )
187 | fi
188 | # Roll the args list around exactly as many times as the number of
189 | # args, so each arg winds up back in the position where it started, but
190 | # possibly modified.
191 | #
192 | # NB: a `for` loop captures its iteration list before it begins, so
193 | # changing the positional parameters here affects neither the number of
194 | # iterations, nor the values presented in `arg`.
195 | shift # remove old arg
196 | set -- "$@" "$arg" # push replacement arg
197 | done
198 | fi
199 |
200 |
201 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
202 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
203 |
204 | # Collect all arguments for the java command;
205 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
206 | # shell script including quotes and variable substitutions, so put them in
207 | # double quotes to make sure that they get re-expanded; and
208 | # * put everything else in single quotes, so that it's not re-expanded.
209 |
210 | set -- \
211 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
212 | -classpath "$CLASSPATH" \
213 | org.gradle.wrapper.GradleWrapperMain \
214 | "$@"
215 |
216 | # Stop when "xargs" is not available.
217 | if ! command -v xargs >/dev/null 2>&1
218 | then
219 | die "xargs is not available"
220 | fi
221 |
222 | # Use "xargs" to parse quoted args.
223 | #
224 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
225 | #
226 | # In Bash we could simply go:
227 | #
228 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
229 | # set -- "${ARGS[@]}" "$@"
230 | #
231 | # but POSIX shell has neither arrays nor command substitution, so instead we
232 | # post-process each arg (as a line of input to sed) to backslash-escape any
233 | # character that might be a shell metacharacter, then use eval to reverse
234 | # that process (while maintaining the separation between arguments), and wrap
235 | # the whole thing up as a single "set" statement.
236 | #
237 | # This will of course break if any of these variables contains a newline or
238 | # an unmatched quote.
239 | #
240 |
241 | eval "set -- $(
242 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
243 | xargs -n1 |
244 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
245 | tr '\n' ' '
246 | )" '"$@"'
247 |
248 | exec "$JAVACMD" "$@"
249 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%"=="" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%"=="" set DIRNAME=.
29 | @rem This is normally unused
30 | set APP_BASE_NAME=%~n0
31 | set APP_HOME=%DIRNAME%
32 |
33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
35 |
36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
38 |
39 | @rem Find java.exe
40 | if defined JAVA_HOME goto findJavaFromJavaHome
41 |
42 | set JAVA_EXE=java.exe
43 | %JAVA_EXE% -version >NUL 2>&1
44 | if %ERRORLEVEL% equ 0 goto execute
45 |
46 | echo.
47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
48 | echo.
49 | echo Please set the JAVA_HOME variable in your environment to match the
50 | echo location of your Java installation.
51 |
52 | goto fail
53 |
54 | :findJavaFromJavaHome
55 | set JAVA_HOME=%JAVA_HOME:"=%
56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
57 |
58 | if exist "%JAVA_EXE%" goto execute
59 |
60 | echo.
61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
62 | echo.
63 | echo Please set the JAVA_HOME variable in your environment to match the
64 | echo location of your Java installation.
65 |
66 | goto fail
67 |
68 | :execute
69 | @rem Setup the command line
70 |
71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
72 |
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if %ERRORLEVEL% equ 0 goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | set EXIT_CODE=%ERRORLEVEL%
85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
87 | exit /b %EXIT_CODE%
88 |
89 | :mainEnd
90 | if "%OS%"=="Windows_NT" endlocal
91 |
92 | :omega
93 |
--------------------------------------------------------------------------------
/jsonpath4k.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/a-sit-plus/jsonpath4k/b72fb2e6179b535820be76d8d1280a60761b455f/jsonpath4k.png
--------------------------------------------------------------------------------
/jsonpath4k/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import com.strumenta.antlrkotlin.gradle.AntlrKotlinTask
2 | import org.apache.tools.ant.taskdefs.condition.Os
3 | import org.jetbrains.dokka.gradle.DokkaTask
4 | import org.jetbrains.dokka.gradle.DokkaTaskPartial
5 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget
6 | import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
7 | import org.jetbrains.kotlin.gradle.dsl.kotlinExtension
8 | import org.jetbrains.kotlin.gradle.plugin.mpp.BitcodeEmbeddingMode
9 | import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
10 | import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFrameworkConfig
11 |
12 | plugins {
13 | alias(libs.plugins.android.library)
14 | alias(libs.plugins.jetbrains.kotlin.multiplatform)
15 | alias(libs.plugins.jetbrains.kotlinx.serialization)
16 | alias(libs.plugins.kotest.multiplatform)
17 | alias(libs.plugins.antlr.kotlin.plugin)
18 | alias(libs.plugins.jetbrains.dokka)
19 | id("maven-publish")
20 | id("signing")
21 | }
22 |
23 | /* required for maven publication */
24 | val artifactVersion: String by extra
25 | group = "at.asitplus"
26 | version = artifactVersion
27 |
28 |
29 | repositories {
30 | google()
31 | maven("https://s01.oss.sonatype.org/content/repositories/snapshots") //KOTEST snapshot
32 | mavenCentral()
33 | }
34 |
35 |
36 | val SRCDIR_ANTRL = "src/gen/kotlin"
37 |
38 | //HACK THE PLANET (i.e. regenerate every time)
39 | val SKIP_GEN = "GRADLE_SKIP_ANTLR_GENT_JSONPATH"
40 | if (System.getenv(SKIP_GEN) != "true") {
41 | layout.projectDirectory.dir(SRCDIR_ANTRL).asFile.deleteRecursively()
42 | println("> Manually invoking generateKotlinGrammarSource ")
43 | Runtime.getRuntime().exec(
44 | arrayOf(if (!Os.isFamily(Os.FAMILY_WINDOWS)) "./gradlew" else "./gradlew.bat", "generateKotlinGrammarSource"),
45 | arrayOf("$SKIP_GEN=true")
46 | ).also { proc ->
47 | proc.errorStream.bufferedReader().forEachLine { System.err.println(it) }
48 | proc.inputStream.bufferedReader().forEachLine { println(it) }
49 | }.waitFor()
50 | }
51 |
52 | val generateKotlinGrammarSource = tasks.register("generateKotlinGrammarSource") {
53 | dependsOn("cleanGenerateKotlinGrammarSource")
54 |
55 | // compiling any *.g4 files within the project
56 | source = fileTree(layout.projectDirectory) {
57 | include("**/*.g4")
58 | }
59 |
60 | // We want the generated source files to have this package name
61 | packageName = "at.asitplus.jsonpath.generated"
62 |
63 | // We want visitors alongside listeners.
64 | // The Kotlin target language is implicit, as is the file encoding (UTF-8)
65 | arguments = listOf("-visitor")
66 |
67 | // Generated files are output inside src/gen/kotlin/{package-name}
68 | val outDir = "$SRCDIR_ANTRL/${packageName!!.replace(".", "/")}"
69 | outputDirectory = layout.projectDirectory.dir(outDir).asFile
70 |
71 | }
72 |
73 |
74 |
75 | kotlin {
76 | jvmToolchain(17)
77 |
78 | jvm {
79 | compilations.all {
80 | kotlinOptions {
81 | jvmTarget = "17"
82 | freeCompilerArgs = listOf(
83 | "-Xjsr305=strict"
84 | )
85 | }
86 | }
87 | }
88 |
89 | androidTarget {
90 | compilerOptions {
91 | publishLibraryVariants("release")
92 | jvmTarget = JvmTarget.JVM_1_8
93 | }
94 | }
95 |
96 |
97 | iosArm64()
98 | iosSimulatorArm64()
99 | iosX64()
100 |
101 |
102 | sourceSets {
103 | commonMain{
104 | kotlin.srcDir(SRCDIR_ANTRL)
105 | dependencies {
106 | implementation(libs.antlr.kotlin)
107 | implementation(libs.jetbrains.kotlinx.serialization)
108 | implementation(libs.napier)
109 | }
110 | }
111 | commonTest {
112 | dependencies {
113 | implementation(libs.kotest.common)
114 | implementation(libs.kotest.property)
115 | implementation(libs.kotest.assertions.core)
116 | implementation(libs.kotest.framework.engine)
117 | implementation(libs.kotest.framework.datatest)
118 | implementation(libs.jetbrains.kotlinx.serialization)
119 | }
120 | }
121 |
122 | jvmTest {
123 | dependencies {
124 | implementation(libs.kotest.runner.junit5)
125 | }
126 | }
127 | }
128 | }
129 |
130 | exportIosFramework("JsonPath4K")
131 |
132 | val javadocJar = setupDokka(
133 | baseUrl = "https://github.com/a-sit-plus/jsonpath4k/tree/main/",
134 | multiModuleDoc = false
135 | )
136 |
137 |
138 | android {
139 | namespace = "at.asitplus.jsonpath4k"
140 | compileSdk = 34
141 | compileOptions {
142 | sourceCompatibility = JavaVersion.VERSION_1_8
143 | targetCompatibility = JavaVersion.VERSION_1_8
144 | }
145 | defaultConfig {
146 | minSdk = 30
147 | }
148 | }
149 |
150 | publishing {
151 | publications {
152 | withType {
153 | if (this.name != "relocation") artifact(javadocJar)
154 | pom {
155 | name.set("JsonPath4K")
156 | description.set("Kotlin Multiplatform library for using Json Paths as specified in [RFC9535](https://datatracker.ietf.org/doc/rfc9535/)")
157 | url.set("https://github.com/a-sit-plus/jsonpath4k")
158 | licenses {
159 | license {
160 | name.set("The Apache License, Version 2.0")
161 | url.set("http://www.apache.org/licenses/LICENSE-2.0.txt")
162 | }
163 | }
164 | developers {
165 | developer {
166 | id.set("acrusage")
167 | name.set("Stefan Kreiner")
168 | email.set("stefan.kreiner@iaik.tugraz.at")
169 | }
170 | developer {
171 | id.set("nodh")
172 | name.set("Christian Kollmann")
173 | email.set("christian.kollmann@a-sit.at")
174 | }
175 | developer {
176 | id.set("JesusMcCloud")
177 | name.set("Bernd Prünster")
178 | email.set("bernd.pruenster@a-sit.at")
179 | }
180 | }
181 | scm {
182 | connection.set("scm:git:git@github.com:a-sit-plus/jsonpath4k.git")
183 | developerConnection.set("scm:git:git@github.com:a-sit-plus/jsonpath4k.git")
184 | url.set("https://github.com/a-sit-plus/jsonpath4k")
185 | }
186 | }
187 | }
188 | }
189 |
190 | repositories {
191 | mavenLocal {
192 | signing.isRequired = false
193 | }
194 | maven {
195 | url = uri(layout.projectDirectory.dir("..").dir("repo"))
196 | name = "local"
197 | signing.isRequired = false
198 | }
199 | }
200 | }
201 |
202 | signing {
203 | val signingKeyId: String? by project
204 | val signingKey: String? by project
205 | val signingPassword: String? by project
206 | useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword)
207 | sign(publishing.publications)
208 | }
209 |
210 |
211 | /**
212 | * taken from vclib conventions plugin at https://github.com/a-sit-plus/gradle-conventions-plugin
213 | */
214 | fun Project.exportIosFramework(
215 | name: String,
216 | vararg additionalExports: Any
217 | ) = exportIosFramework(name, bitcodeEmbeddingMode = BitcodeEmbeddingMode.BITCODE, additionalExports = additionalExports)
218 |
219 | fun Project.exportIosFramework(
220 | name: String,
221 | bitcodeEmbeddingMode: BitcodeEmbeddingMode,
222 | vararg additionalExports: Any
223 | ) {
224 | val iosTargets = kotlinExtension.let {
225 | if (it is KotlinMultiplatformExtension) {
226 | it.targets.filterIsInstance().filter { it.name.startsWith("ios") }
227 | } else throw StopExecutionException("No iOS Targets found! Declare them explicitly before calling exportIosFramework!")
228 | }
229 |
230 | extensions.getByType().apply {
231 | XCFrameworkConfig(project, name).also { xcf ->
232 | logger.lifecycle(" \u001B[1mXCFrameworks will be exported for the following iOS targets: ${iosTargets.joinToString { it.name }}\u001B[0m")
233 | iosTargets.forEach {
234 | it.binaries.framework {
235 | baseName = name
236 | embedBitcode(bitcodeEmbeddingMode)
237 | additionalExports.forEach { export(it) }
238 | xcf.add(this)
239 | }
240 | }
241 | }
242 | }
243 | }
244 |
245 | fun Project.setupDokka(
246 | outputDir: String = rootProject.layout.buildDirectory.dir("dokka").get().asFile.canonicalPath,
247 | baseUrl: String,
248 | multiModuleDoc: Boolean = false,
249 | remoteLineSuffix: String = "#L"
250 | ): TaskProvider {
251 | val dokkaHtml = (tasks["dokkaHtml"] as DokkaTask).apply { outputDirectory.set(file(outputDir)) }
252 |
253 | val deleteDokkaOutput = tasks.register("deleteDokkaOutputDirectory") {
254 | delete(outputDir)
255 | }
256 | val sourceLinktToConfigure = if (multiModuleDoc) (tasks["dokkaHtmlPartial"] as DokkaTaskPartial) else dokkaHtml
257 | sourceLinktToConfigure.dokkaSourceSets.configureEach {
258 | sourceLink {
259 | localDirectory.set(file("src/$name/kotlin"))
260 | remoteUrl.set(uri("$baseUrl/${project.name}/src/$name/kotlin").toURL())
261 | this@sourceLink.remoteLineSuffix.set(remoteLineSuffix)
262 | }
263 | }
264 |
265 | return tasks.register("javadocJar") {
266 | dependsOn(deleteDokkaOutput, dokkaHtml)
267 | archiveClassifier.set("javadoc")
268 | from(outputDir)
269 | }
270 | }
271 |
272 |
273 | afterEvaluate {
274 | tasks.withType {
275 | useJUnitPlatform()
276 | }
277 |
278 | /**
279 | * Makes all publishing tasks depend on all signing tasks. Hampers parallelization, but works around dodgy task dependencies
280 | * which (more often than anticipated) makes the build process stumble over its own feet.
281 | */
282 |
283 | tasks.withType().also { signingTasks ->
284 | if (signingTasks.isNotEmpty()) {
285 | logger.lifecycle("> Making signing tasks of project \u001B[1m$name\u001B[0m run after publish tasks")
286 | tasks.withType().configureEach {
287 | mustRunAfter(*signingTasks.toTypedArray())
288 | logger.lifecycle(" * $name must now run after ${signingTasks.joinToString { it.name }}")
289 | }
290 | logger.lifecycle("")
291 | }
292 | }
293 |
294 | }
295 |
--------------------------------------------------------------------------------
/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/JsonPath.kt:
--------------------------------------------------------------------------------
1 | package at.asitplus.jsonpath
2 |
3 | import at.asitplus.jsonpath.core.JsonPathCompiler
4 | import at.asitplus.jsonpath.core.JsonPathFunctionExtension
5 | import at.asitplus.jsonpath.core.NodeList
6 | import kotlinx.serialization.json.JsonElement
7 |
8 | class JsonPath(
9 | jsonPathExpression: String,
10 | compiler: JsonPathCompiler = JsonPathDependencyManager.compiler,
11 | functionExtensionRetriever: (String) -> JsonPathFunctionExtension<*>? = JsonPathDependencyManager.functionExtensionRepository::getExtension
12 | ) {
13 | private val query = compiler.compile(
14 | jsonPath = jsonPathExpression,
15 | functionExtensionRetriever = functionExtensionRetriever,
16 | )
17 |
18 | fun query(jsonElement: JsonElement): NodeList {
19 | return query.invoke(jsonElement)
20 | }
21 | }
--------------------------------------------------------------------------------
/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/JsonPathDependencyManager.kt:
--------------------------------------------------------------------------------
1 | package at.asitplus.jsonpath
2 |
3 | import at.asitplus.jsonpath.core.JsonPathCompiler
4 | import at.asitplus.jsonpath.core.JsonPathFilterExpressionType
5 | import at.asitplus.jsonpath.core.JsonPathFunctionExtension
6 | import at.asitplus.jsonpath.core.functionExtensions.lengthFunctionExtension
7 | import at.asitplus.jsonpath.core.functionExtensions.matchFunctionExtension
8 | import at.asitplus.jsonpath.core.functionExtensions.searchFunctionExtension
9 | import at.asitplus.jsonpath.core.functionExtensions.valueFunctionExtension
10 | import at.asitplus.jsonpath.core.functionExtensions.countFunctionExtension
11 | import at.asitplus.jsonpath.implementation.AntlrJsonPathCompiler
12 | import at.asitplus.jsonpath.implementation.AntlrJsonPathCompilerErrorListener
13 | import io.github.aakira.napier.Napier
14 | import org.antlr.v4.kotlinruntime.BaseErrorListener
15 | import org.antlr.v4.kotlinruntime.RecognitionException
16 | import org.antlr.v4.kotlinruntime.Recognizer
17 |
18 | object JsonPathDependencyManager {
19 | /**
20 | * Function extension repository that may be extended with custom functions by the user of this library.
21 | */
22 | var functionExtensionRepository: JsonPathFunctionExtensionRepository =
23 | JsonPathFunctionExtensionMapRepository(
24 | listOf(
25 | lengthFunctionExtension,
26 | countFunctionExtension,
27 | matchFunctionExtension,
28 | searchFunctionExtension,
29 | valueFunctionExtension,
30 | ).toMap().toMutableMap()
31 | )
32 |
33 | var compiler: JsonPathCompiler = AntlrJsonPathCompiler(
34 | errorListener = napierAntlrJsonPathCompilerErrorListener,
35 | )
36 | }
37 |
38 | private val napierAntlrJsonPathCompilerErrorListener by lazy {
39 | object : AntlrJsonPathCompilerErrorListener, BaseErrorListener() {
40 | override fun unknownFunctionExtension(functionExtensionName: String) {
41 | Napier.e {
42 | "Unknown JSONPath function extension: \"$functionExtensionName\""
43 | }
44 | }
45 |
46 | override fun invalidFunctionExtensionForTestExpression(functionExtensionName: String) {
47 | Napier.e {
48 | "Invalid JSONPath function extension return type for test expression: \"$functionExtensionName\""
49 | }
50 | }
51 |
52 | override fun invalidFunctionExtensionForComparable(functionExtensionName: String) {
53 | Napier.e {
54 | "Invalid JSONPath function extension return type for comparable expression: \"$functionExtensionName\""
55 | }
56 | }
57 |
58 | override fun invalidArglistForFunctionExtension(
59 | functionExtensionName: String,
60 | functionExtensionImplementation: JsonPathFunctionExtension<*>,
61 | coercedArgumentTypes: List>
62 | ) {
63 | Napier.e {
64 | "Invalid arguments for function extension \"$functionExtensionName\": Expected: <${
65 | functionExtensionImplementation.argumentTypes.joinToString(
66 | ", "
67 | )
68 | }>, but received <${
69 | coercedArgumentTypes.map { it.first }.joinToString(", ")
70 | }>: <${coercedArgumentTypes.map { it.second }.joinToString(", ")}>"
71 | }
72 | }
73 |
74 | override fun invalidTestExpression(testContextString: String) {
75 | Napier.e {
76 | "Invalid test expression: $testContextString"
77 | }
78 | }
79 |
80 | override fun syntaxError(
81 | recognizer: Recognizer<*, *>,
82 | offendingSymbol: Any?,
83 | line: Int,
84 | charPositionInLine: Int,
85 | msg: String,
86 | e: RecognitionException?
87 | ) {
88 | Napier.e {
89 | "Syntax error $line:$charPositionInLine $msg"
90 | }
91 | }
92 | }
93 | }
--------------------------------------------------------------------------------
/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/JsonPathFunctionExtensionMapRepository.kt:
--------------------------------------------------------------------------------
1 | package at.asitplus.jsonpath
2 |
3 | import at.asitplus.jsonpath.core.JsonPathFunctionExtension
4 |
5 | internal class JsonPathFunctionExtensionMapRepository(
6 | private val extensions: MutableMap> = mutableMapOf()
7 | ) : JsonPathFunctionExtensionRepository {
8 | override fun addExtension(
9 | name: String,
10 | extension: () -> JsonPathFunctionExtension<*>
11 | ) {
12 | extensions[name]?.let {
13 | throw FunctionExtensionCollisionException(
14 | "A function extension with the name \"$name\" has already been added: $it"
15 | )
16 | }
17 | extensions[name] = extension()
18 | }
19 |
20 | override fun getExtension(name: String): JsonPathFunctionExtension<*>? = extensions[name]
21 | override fun export(): Map> = extensions.toMap()
22 | }
--------------------------------------------------------------------------------
/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/JsonPathFunctionExtensionRepository.kt:
--------------------------------------------------------------------------------
1 | package at.asitplus.jsonpath
2 |
3 | import at.asitplus.jsonpath.core.JsonPathFunctionExtension
4 |
5 | /**
6 | * This class is not specified in the rfc standard, it's but an implementation detail.
7 | * It's a way to provide users with a way to add custom function extensions.
8 | */
9 | interface JsonPathFunctionExtensionRepository {
10 | /**
11 | * Implementations should throw FunctionExtensionCollisionException if an extension with that name already exists.
12 | */
13 | fun addExtension(name: String, extension: () -> JsonPathFunctionExtension<*>)
14 | fun getExtension(name: String): JsonPathFunctionExtension<*>?
15 | fun export(): Map>
16 | }
17 |
18 | open class FunctionExtensionCollisionException(message: String) : Exception(message)
--------------------------------------------------------------------------------
/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/core/JsonPathCompiler.kt:
--------------------------------------------------------------------------------
1 | package at.asitplus.jsonpath.core
2 |
3 | interface JsonPathCompiler {
4 | fun compile(
5 | jsonPath: String,
6 | functionExtensionRetriever: (String) -> JsonPathFunctionExtension<*>?
7 | ): JsonPathQuery
8 | }
--------------------------------------------------------------------------------
/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/core/JsonPathExceptions.kt:
--------------------------------------------------------------------------------
1 | package at.asitplus.jsonpath.core
2 |
3 | open class JsonPathCompilerException(message: String) : Exception(message)
4 |
5 | open class JsonPathQueryException(message: String) : Exception(message)
--------------------------------------------------------------------------------
/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/core/JsonPathFilterExpressionType.kt:
--------------------------------------------------------------------------------
1 | package at.asitplus.jsonpath.core
2 |
3 | /**
4 | * specification: https://datatracker.ietf.org/doc/rfc9535/
5 | * date: 2024-02
6 | * section: 2.4.1. Type System for Function Expressions
7 | */
8 | enum class JsonPathFilterExpressionType {
9 | ValueType,
10 | LogicalType,
11 | NodesType;
12 | }
--------------------------------------------------------------------------------
/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/core/JsonPathFilterExpressionValue.kt:
--------------------------------------------------------------------------------
1 | package at.asitplus.jsonpath.core
2 |
3 | import kotlinx.serialization.json.JsonElement
4 |
5 | /**
6 | * specification: https://datatracker.ietf.org/doc/rfc9535/
7 | * date: 2024-02
8 | * section: 2.4.1. Type System for Function Expressions
9 | */
10 | sealed interface JsonPathFilterExpressionValue {
11 | val expressionType: JsonPathFilterExpressionType
12 |
13 | sealed class ValueTypeValue : JsonPathFilterExpressionValue {
14 | override val expressionType: JsonPathFilterExpressionType =
15 | JsonPathFilterExpressionType.ValueType
16 | data class JsonValue(val jsonElement: JsonElement) : ValueTypeValue()
17 | data object Nothing : ValueTypeValue()
18 | }
19 |
20 | data class LogicalTypeValue(val isTrue: Boolean) : JsonPathFilterExpressionValue {
21 | override val expressionType: JsonPathFilterExpressionType =
22 | JsonPathFilterExpressionType.LogicalType
23 | }
24 |
25 | sealed class NodesTypeValue(open val nodeList: List) :
26 | JsonPathFilterExpressionValue {
27 | override val expressionType: JsonPathFilterExpressionType =
28 | JsonPathFilterExpressionType.NodesType
29 |
30 | sealed class FilterQueryResult(nodeList: List): NodesTypeValue(nodeList) {
31 | data class SingularQueryResult(override val nodeList: List): FilterQueryResult(nodeList)
32 | data class NonSingularQueryResult(override val nodeList: List): FilterQueryResult(nodeList)
33 | }
34 | data class FunctionExtensionResult(override val nodeList: List): NodesTypeValue(nodeList)
35 | }
36 | }
--------------------------------------------------------------------------------
/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/core/JsonPathFunctionExtension.kt:
--------------------------------------------------------------------------------
1 | package at.asitplus.jsonpath.core
2 |
3 | import kotlinx.serialization.json.JsonElement
4 |
5 | /**
6 | * specification: https://datatracker.ietf.org/doc/rfc9535/
7 | * date: 2024-02
8 | * section: 2.4. Function Extensions
9 | */
10 | sealed class JsonPathFunctionExtension(
11 | vararg val argumentTypes: JsonPathFilterExpressionType,
12 | ) {
13 | abstract fun evaluate(arguments: List): ReturnType
14 |
15 | class ValueTypeFunctionExtension(
16 | vararg argumentTypes: JsonPathFilterExpressionType,
17 | private val evaluator: (arguments: List) -> JsonElement?
18 | ) : JsonPathFunctionExtension(
19 | argumentTypes = argumentTypes,
20 | ) {
21 | override fun evaluate(arguments: List): JsonPathFilterExpressionValue.ValueTypeValue {
22 | return evaluator(arguments)?.let {
23 | JsonPathFilterExpressionValue.ValueTypeValue.JsonValue(it)
24 | } ?: JsonPathFilterExpressionValue.ValueTypeValue.Nothing
25 | }
26 | }
27 |
28 | class LogicalTypeFunctionExtension(
29 | vararg argumentTypes: JsonPathFilterExpressionType,
30 | private val evaluator: (arguments: List) -> Boolean
31 | ) : JsonPathFunctionExtension(
32 | argumentTypes = argumentTypes,
33 | ) {
34 | override fun evaluate(arguments: List): JsonPathFilterExpressionValue.LogicalTypeValue {
35 | return JsonPathFilterExpressionValue.LogicalTypeValue(evaluator(arguments))
36 | }
37 | }
38 |
39 | class NodesTypeFunctionExtension(
40 | vararg argumentTypes: JsonPathFilterExpressionType,
41 | private val evaluator: (arguments: List) -> List
42 | ) : JsonPathFunctionExtension(
43 | argumentTypes = argumentTypes,
44 | ) {
45 | override fun evaluate(arguments: List): JsonPathFilterExpressionValue.NodesTypeValue.FunctionExtensionResult {
46 | return JsonPathFilterExpressionValue.NodesTypeValue.FunctionExtensionResult(
47 | evaluator(arguments)
48 | )
49 | }
50 | }
51 | }
--------------------------------------------------------------------------------
/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/core/JsonPathQuery.kt:
--------------------------------------------------------------------------------
1 | package at.asitplus.jsonpath.core
2 |
3 | import kotlinx.serialization.json.JsonElement
4 |
5 | interface JsonPathQuery {
6 | fun invoke(currentNode: JsonElement, rootNode: JsonElement = currentNode): NodeList
7 | }
--------------------------------------------------------------------------------
/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/core/JsonPathSelector.kt:
--------------------------------------------------------------------------------
1 | package at.asitplus.jsonpath.core
2 |
3 | import at.asitplus.jsonpath.core.NormalizedJsonPathSegment.IndexSegment
4 | import at.asitplus.jsonpath.core.NormalizedJsonPathSegment.NameSegment
5 | import kotlinx.serialization.json.JsonArray
6 | import kotlinx.serialization.json.JsonElement
7 | import kotlinx.serialization.json.JsonObject
8 | import kotlinx.serialization.json.JsonPrimitive
9 | import kotlin.math.max
10 | import kotlin.math.min
11 |
12 | /**
13 | * specification: https://datatracker.ietf.org/doc/rfc9535/
14 | * date: 2024-02
15 | * section: 2.3. Selectors
16 | */
17 | internal sealed interface JsonPathSelector {
18 | fun invoke(
19 | currentNode: JsonElement,
20 | rootNode: JsonElement = currentNode,
21 | ): NodeList
22 |
23 | data object RootSelector : JsonPathSelector {
24 | override fun invoke(
25 | currentNode: JsonElement,
26 | rootNode: JsonElement,
27 | ): NodeList {
28 | return listOf(
29 | NodeListEntry(
30 | normalizedJsonPath = NormalizedJsonPath(),
31 | value = rootNode
32 | )
33 | )
34 | }
35 | }
36 |
37 | data object CurrentNodeSelector : JsonPathSelector {
38 | override fun invoke(
39 | currentNode: JsonElement,
40 | rootNode: JsonElement,
41 | ): NodeList {
42 | return listOf(
43 | NodeListEntry(
44 | normalizedJsonPath = NormalizedJsonPath(),
45 | value = currentNode
46 | )
47 | )
48 | }
49 | }
50 |
51 | /**
52 | * specification: https://datatracker.ietf.org/doc/rfc9535/
53 | * date: 2024-02
54 | * section: 2.3.1. Name Selector
55 | */
56 | data class MemberSelector(val memberName: String) : JsonPathSelector {
57 | override fun invoke(
58 | currentNode: JsonElement,
59 | rootNode: JsonElement,
60 | ): NodeList {
61 | return when (currentNode) {
62 | is JsonPrimitive -> listOf()
63 |
64 | is JsonArray -> listOf()
65 |
66 | is JsonObject -> listOfNotNull(currentNode[memberName]?.let {
67 | NodeListEntry(
68 | normalizedJsonPath = NormalizedJsonPath(NameSegment(memberName)),
69 | value = it
70 | )
71 | })
72 | }
73 | }
74 | }
75 |
76 | /**
77 | * specification: https://datatracker.ietf.org/doc/rfc9535/
78 | * date: 2024-02
79 | * section: 2.3.3. Index Selector
80 | */
81 | data class IndexSelector(val index: Int) : JsonPathSelector {
82 | override fun invoke(
83 | currentNode: JsonElement,
84 | rootNode: JsonElement,
85 | ): NodeList {
86 | return when (currentNode) {
87 | is JsonPrimitive -> listOf()
88 |
89 | is JsonArray -> {
90 | val actualIndex = if (index >= 0) {
91 | index
92 | } else {
93 | index + currentNode.size
94 | }
95 | listOfNotNull(
96 | currentNode.getOrNull(actualIndex)?.let {
97 | NodeListEntry(
98 | normalizedJsonPath = NormalizedJsonPath(IndexSegment(actualIndex.toUInt())),
99 | value = it
100 | )
101 | }
102 | )
103 | }
104 |
105 |
106 | is JsonObject -> listOf()
107 | }
108 | }
109 | }
110 |
111 | /**
112 | * specification: https://datatracker.ietf.org/doc/rfc9535/
113 | * date: 2024-02
114 | * section: 2.3.2. Wildcard Selector
115 | */
116 | data object WildCardSelector : JsonPathSelector {
117 | override fun invoke(
118 | currentNode: JsonElement,
119 | rootNode: JsonElement,
120 | ): NodeList {
121 | return when (currentNode) {
122 | is JsonPrimitive -> listOf()
123 |
124 | is JsonArray -> currentNode.indices.flatMap {
125 | IndexSelector(it).invoke(
126 | currentNode = currentNode,
127 | rootNode = rootNode,
128 | )
129 | }
130 |
131 | is JsonObject -> currentNode.keys.flatMap {
132 | MemberSelector(it).invoke(
133 | currentNode = currentNode,
134 | rootNode = rootNode,
135 | )
136 | }
137 | }
138 | }
139 | }
140 |
141 | /**
142 | * specification: https://datatracker.ietf.org/doc/rfc9535/
143 | * date: 2024-02
144 | * section: 2.3.2. Wildcard Selector
145 | */
146 | data class BracketedSelector(val selectors: List) : JsonPathSelector {
147 | override fun invoke(
148 | currentNode: JsonElement,
149 | rootNode: JsonElement,
150 | ): NodeList {
151 | return selectors.flatMap {
152 | it.invoke(
153 | currentNode = currentNode,
154 | rootNode = rootNode,
155 | )
156 | }
157 | }
158 | }
159 |
160 | /**
161 | * specification: https://datatracker.ietf.org/doc/rfc9535/
162 | * date: 2024-02
163 | * section: 2.3.4. Array Slice Selector
164 | */
165 | data class SliceSelector(
166 | val startInclusive: Int? = null,
167 | val endExclusive: Int? = null,
168 | val step: Int? = null,
169 | ) : JsonPathSelector {
170 | override fun invoke(
171 | currentNode: JsonElement,
172 | rootNode: JsonElement,
173 | ): NodeList {
174 | return when (currentNode) {
175 | is JsonPrimitive -> listOf()
176 |
177 | is JsonArray -> {
178 | // The default value for step is 1.
179 | val actualStepSize = step ?: 1
180 |
181 | // When step is 0, no elements are selected.
182 | if (actualStepSize == 0) {
183 | return listOf()
184 | }
185 |
186 | // default start and end according to specification
187 | val start = startInclusive
188 | ?: if (actualStepSize > 0) 0 else currentNode.size - 1
189 | val end = endExclusive
190 | ?: if (actualStepSize > 0) currentNode.size else -currentNode.size - 1
191 |
192 | val (lower, upper) = bounds(start, end, actualStepSize, currentNode.size)
193 |
194 | val range = if (actualStepSize > 0) {
195 | lower..
201 | IndexSelector(index).invoke(
202 | currentNode = currentNode,
203 | rootNode = rootNode,
204 | )
205 | }
206 | }
207 |
208 | is JsonObject -> listOf()
209 | }
210 | }
211 |
212 | private fun normalize(index: Int, arrayLength: Int): Int {
213 | return if (index >= 0) {
214 | index
215 | } else {
216 | arrayLength + index
217 | }
218 | }
219 |
220 | private fun bounds(start: Int, end: Int, stepSize: Int, arrayLength: Int): Pair {
221 | val normalizedStart = normalize(start, arrayLength)
222 | val normalizedEnd = normalize(end, arrayLength)
223 |
224 | // implementation bounds according to specification
225 | return if (stepSize >= 0) {
226 | val lower = min(max(normalizedStart, 0), arrayLength)
227 | val upper = min(max(normalizedEnd, 0), arrayLength)
228 | lower to upper
229 | } else {
230 | val upper = min(max(normalizedStart, -1), arrayLength - 1)
231 | val lower = min(max(normalizedEnd, -1), arrayLength - 1)
232 | lower to upper
233 | }
234 | }
235 | }
236 |
237 | /**
238 | * specification: https://datatracker.ietf.org/doc/rfc9535/
239 | * date: 2024-02
240 | * section: 2.5.2. Descendant Segment
241 | */
242 | data class DescendantSelector(
243 | val selector: JsonPathSelector,
244 | ) : JsonPathSelector {
245 | override fun invoke(
246 | currentNode: JsonElement,
247 | rootNode: JsonElement,
248 | ): NodeList {
249 | // For each i such that 1 <= i <= n, the nodelist Ri is defined to be a
250 | // result of applying the child segment [] to the node Di.
251 | return when (currentNode) {
252 | is JsonPrimitive -> listOf()
253 |
254 | is JsonArray -> CurrentNodeSelector.invoke(
255 | currentNode = currentNode,
256 | rootNode = rootNode,
257 | ).flatMap { descendant ->
258 | selector.invoke(
259 | currentNode = descendant.value,
260 | rootNode = rootNode,
261 | ).map {
262 | NodeListEntry(
263 | normalizedJsonPath = descendant.normalizedJsonPath + it.normalizedJsonPath,
264 | value = it.value,
265 | )
266 | }
267 | } + currentNode.flatMapIndexed { index, childNode ->
268 | invoke(
269 | currentNode = childNode,
270 | rootNode = rootNode
271 | ).map {
272 | NodeListEntry(
273 | normalizedJsonPath = NormalizedJsonPath(IndexSegment(index.toUInt())) + it.normalizedJsonPath,
274 | it.value
275 | )
276 | }
277 | }
278 |
279 | is JsonObject -> CurrentNodeSelector.invoke(
280 | currentNode = currentNode,
281 | rootNode = rootNode,
282 | ).flatMap { descendant ->
283 | selector.invoke(
284 | currentNode = descendant.value,
285 | rootNode = rootNode,
286 | ).map {
287 | NodeListEntry(
288 | normalizedJsonPath = descendant.normalizedJsonPath + it.normalizedJsonPath,
289 | value = it.value,
290 | )
291 | }
292 | } + currentNode.flatMap { entry ->
293 | invoke(entry.value, rootNode).map {
294 | NodeListEntry(
295 | normalizedJsonPath = NormalizedJsonPath(NameSegment(entry.key)) + it.normalizedJsonPath,
296 | it.value
297 | )
298 | }
299 | }
300 | }
301 | }
302 | }
303 |
304 | /**
305 | * specification: https://datatracker.ietf.org/doc/rfc9535/
306 | * date: 2024-02
307 | * section: 2.3.5. Filter Selector
308 | */
309 | data class FilterSelector(
310 | private val filterPredicate: FilterPredicate,
311 | ) : JsonPathSelector {
312 | override fun invoke(
313 | currentNode: JsonElement,
314 | rootNode: JsonElement,
315 | ): NodeList {
316 | return when (currentNode) {
317 | is JsonPrimitive -> listOf()
318 |
319 | is JsonArray -> currentNode.flatMapIndexed { index, _ ->
320 | IndexSelector(index).invoke(
321 | currentNode = currentNode,
322 | rootNode = rootNode,
323 | )
324 | }
325 |
326 | is JsonObject -> currentNode.entries.flatMap {
327 | MemberSelector(it.key).invoke(
328 | currentNode = currentNode,
329 | rootNode = rootNode,
330 | )
331 | }
332 | }.filter {
333 | filterPredicate.invoke(
334 | currentNode = it.value,
335 | rootNode = rootNode,
336 | )
337 | }
338 | }
339 | }
340 | }
341 |
342 | interface FilterPredicate {
343 | fun invoke(
344 | currentNode: JsonElement,
345 | rootNode: JsonElement,
346 | ): Boolean
347 | }
--------------------------------------------------------------------------------
/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/core/JsonPathSelectorQuery.kt:
--------------------------------------------------------------------------------
1 | package at.asitplus.jsonpath.core
2 |
3 | import kotlinx.serialization.json.JsonElement
4 |
5 |
6 | internal class JsonPathSelectorQuery(
7 | private val selectors: List,
8 | ) : JsonPathQuery {
9 | override fun invoke(currentNode: JsonElement, rootNode: JsonElement): NodeList {
10 | var matches = selectors.firstOrNull()?.invoke(
11 | currentNode = currentNode,
12 | rootNode = rootNode,
13 | ) ?: listOf()
14 | selectors.forEachIndexed { index, selector ->
15 | matches = if(index == 0) matches else matches.flatMap { match ->
16 | selector.invoke(
17 | currentNode = match.value,
18 | rootNode = rootNode,
19 | ).map { newMatch ->
20 | NodeListEntry(
21 | normalizedJsonPath = match.normalizedJsonPath + newMatch.normalizedJsonPath,
22 | value = newMatch.value
23 | )
24 | }
25 | }
26 | }
27 | return matches
28 | }
29 |
30 | val isSingularQuery: Boolean
31 | get() = selectors.all { // 2.3.5.1. Syntax: https://datatracker.ietf.org/doc/rfc9535/
32 | when(it) {
33 | JsonPathSelector.RootSelector,
34 | JsonPathSelector.CurrentNodeSelector,
35 | is JsonPathSelector.MemberSelector,
36 | is JsonPathSelector.IndexSelector -> true
37 | else -> false
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/core/NodeList.kt:
--------------------------------------------------------------------------------
1 | package at.asitplus.jsonpath.core
2 |
3 | import kotlinx.serialization.Serializable
4 | import kotlinx.serialization.json.JsonElement
5 |
6 | typealias NodeList = List
7 |
8 | @Serializable
9 | data class NodeListEntry(
10 | val normalizedJsonPath: NormalizedJsonPath,
11 | val value: JsonElement,
12 | )
13 |
--------------------------------------------------------------------------------
/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/core/NormalizedJsonPath.kt:
--------------------------------------------------------------------------------
1 | package at.asitplus.jsonpath.core
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | /**
6 | * specification: https://datatracker.ietf.org/doc/rfc9535/
7 | * date: 2024-02
8 | * section: 2.7. Normalized Paths
9 | */
10 | @Serializable
11 | class NormalizedJsonPath(
12 | val segments: List = listOf(),
13 | ) {
14 | constructor(vararg segments: NormalizedJsonPathSegment) : this(segments = segments.asList())
15 | operator fun plus(other: NormalizedJsonPath): NormalizedJsonPath {
16 | return NormalizedJsonPath(this.segments + other.segments)
17 | }
18 |
19 | override fun toString(): String {
20 | return "$${segments.joinToString("")}"
21 | }
22 | }
--------------------------------------------------------------------------------
/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/core/NormalizedJsonPathSegment.kt:
--------------------------------------------------------------------------------
1 | package at.asitplus.jsonpath.core
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | /**
6 | * specification: https://datatracker.ietf.org/doc/rfc9535/
7 | * date: 2024-02
8 | * section: 2.7. Normalized Paths
9 | */
10 | @Serializable
11 | sealed interface NormalizedJsonPathSegment {
12 | @Serializable
13 | class NameSegment(val memberName: String) : NormalizedJsonPathSegment {
14 | override fun toString(): String {
15 | return "[${Rfc9535Utils.escapeToSingleQuotedStringLiteral(memberName)}]"
16 | }
17 | }
18 | @Serializable
19 | class IndexSegment(val index: UInt) : NormalizedJsonPathSegment {
20 | override fun toString(): String {
21 | return "[$index]"
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/core/Rfc8259Utils.kt:
--------------------------------------------------------------------------------
1 | package at.asitplus.jsonpath.core
2 |
3 | import kotlinx.serialization.encodeToString
4 | import kotlinx.serialization.json.Json
5 | import kotlinx.serialization.json.JsonPrimitive
6 |
7 | internal object Rfc8259Utils {
8 | fun unpackStringLiteral(string: String): String {
9 | return Json.decodeFromString(string).content
10 | }
11 |
12 | fun escapeToDoubleQuotedString(string: String): String {
13 | return Json.encodeToString(JsonPrimitive(string))
14 | }
15 | }
--------------------------------------------------------------------------------
/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/core/Rfc9535Utils.kt:
--------------------------------------------------------------------------------
1 | package at.asitplus.jsonpath.core
2 |
3 | internal object Rfc9535Utils {
4 | fun switchToDoubleQuotedString(string: String) = if (string.startsWith("\"")) {
5 | string // treat as normal rfc8259 double quoted string
6 | } else {
7 | // switch to double quoted string
8 | string.substring(1, string.lastIndex)
9 | .replace("\\'", "'")
10 | .replace("\"", "\\\"")
11 | .let {
12 | "\"$it\""
13 | }
14 | }
15 |
16 | fun switchToSingleQuotedString(string: String) = if (string.startsWith("'")) {
17 | string
18 | } else {
19 | // switch to single quoted string
20 | string.substring(1, string.lastIndex)
21 | .replace("'", "\\'")
22 | .replace("\\\"", "\"")
23 | .let {
24 | "'$it'"
25 | }
26 | }
27 |
28 | fun unpackStringLiteral(string: String): String {
29 | val doubleQuoted = switchToDoubleQuotedString(string)
30 | return Rfc8259Utils.unpackStringLiteral(doubleQuoted)
31 | }
32 |
33 | fun escapeToSingleQuotedStringLiteral(string: String): String {
34 | val encoded = escapeToDoubleQuoted(string)
35 | return switchToSingleQuotedString(encoded)
36 | }
37 | fun escapeToDoubleQuoted(string: String): String {
38 | return Rfc8259Utils.escapeToDoubleQuotedString(string)
39 | }
40 | }
--------------------------------------------------------------------------------
/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/core/functionExtensions/countFunctionExtension.kt:
--------------------------------------------------------------------------------
1 | package at.asitplus.jsonpath.core.functionExtensions
2 |
3 | import at.asitplus.jsonpath.core.JsonPathFilterExpressionValue
4 | import at.asitplus.jsonpath.core.JsonPathFilterExpressionType
5 | import at.asitplus.jsonpath.core.JsonPathFunctionExtension
6 | import kotlinx.serialization.ExperimentalSerializationApi
7 | import kotlinx.serialization.json.JsonPrimitive
8 |
9 | /**
10 | * specification: https://datatracker.ietf.org/doc/rfc9535/
11 | * date: 2024-02
12 | * section: 2.4.5. count() Function Extension
13 | */
14 | @OptIn(ExperimentalSerializationApi::class)
15 | internal val countFunctionExtension by lazy {
16 | "count" to JsonPathFunctionExtension.ValueTypeFunctionExtension(
17 | JsonPathFilterExpressionType.NodesType,
18 | ) {
19 | val nodesTypeValue = it[0] as JsonPathFilterExpressionValue.NodesTypeValue
20 | JsonPrimitive(nodesTypeValue.nodeList.size.toUInt())
21 | }
22 | }
--------------------------------------------------------------------------------
/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/core/functionExtensions/lengthFunctionExtension.kt:
--------------------------------------------------------------------------------
1 | package at.asitplus.jsonpath.core.functionExtensions
2 |
3 | import at.asitplus.jsonpath.core.JsonPathFilterExpressionType
4 | import at.asitplus.jsonpath.core.JsonPathFilterExpressionValue
5 | import at.asitplus.jsonpath.core.JsonPathFunctionExtension
6 | import kotlinx.serialization.ExperimentalSerializationApi
7 | import kotlinx.serialization.json.JsonArray
8 | import kotlinx.serialization.json.JsonObject
9 | import kotlinx.serialization.json.JsonPrimitive
10 |
11 | /**
12 | * specification: https://datatracker.ietf.org/doc/rfc9535/
13 | * date: 2024-02
14 | * section: 2.4.4. length() Function Extension
15 | */
16 | @OptIn(ExperimentalSerializationApi::class)
17 | internal val lengthFunctionExtension by lazy {
18 | "length" to JsonPathFunctionExtension.ValueTypeFunctionExtension(
19 | JsonPathFilterExpressionType.ValueType,
20 | ) {
21 | val argument = it[0] as JsonPathFilterExpressionValue.ValueTypeValue
22 |
23 | if (argument !is JsonPathFilterExpressionValue.ValueTypeValue.JsonValue) {
24 | null
25 | } else when (argument.jsonElement) {
26 | is JsonArray -> JsonPrimitive(argument.jsonElement.size.toUInt())
27 |
28 | is JsonObject -> JsonPrimitive(argument.jsonElement.size.toUInt())
29 |
30 | is JsonPrimitive -> if (argument.jsonElement.isString) {
31 | JsonPrimitive(
32 | run {
33 | val codePoints =
34 | argument.jsonElement.content.count() + argument.jsonElement.content.count {
35 | it.code > 0xffff
36 | }
37 | codePoints.toUInt()
38 | }
39 | )
40 | } else null
41 | }
42 | }
43 | }
--------------------------------------------------------------------------------
/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/core/functionExtensions/matchFunctionExtension.kt:
--------------------------------------------------------------------------------
1 | package at.asitplus.jsonpath.core.functionExtensions
2 |
3 | import at.asitplus.jsonpath.core.JsonPathFilterExpressionType
4 | import at.asitplus.jsonpath.core.JsonPathFilterExpressionValue
5 | import at.asitplus.jsonpath.core.JsonPathFunctionExtension
6 | import kotlinx.serialization.json.JsonPrimitive
7 |
8 | /**
9 | * specification: https://datatracker.ietf.org/doc/rfc9535/
10 | * date: 2024-02
11 | * section: 2.4.6. match() Function Extension
12 | */
13 | internal val matchFunctionExtension by lazy {
14 | "match" to JsonPathFunctionExtension.LogicalTypeFunctionExtension(
15 | JsonPathFilterExpressionType.ValueType,
16 | JsonPathFilterExpressionType.ValueType,
17 | ) {
18 | val stringArgument = it[0] as JsonPathFilterExpressionValue.ValueTypeValue
19 | val regexArgument = it[1] as JsonPathFilterExpressionValue.ValueTypeValue
20 |
21 | val stringValue = unpackArgumentToStringValue(stringArgument)
22 | val regexValue = unpackArgumentToStringValue(regexArgument)
23 |
24 | if(stringValue == null || regexValue == null) {
25 | false
26 | } else {
27 | try {
28 | // TODO: check assumption that Regex supports RFC9485:
29 | // https://www.rfc-editor.org/rfc/rfc9485.html
30 | Regex(regexValue).matches(stringValue)
31 | } catch (throwable: Throwable) {
32 | false
33 | }
34 | }
35 | }
36 | }
37 |
38 | private fun unpackArgumentToStringValue(
39 | valueArgument: JsonPathFilterExpressionValue.ValueTypeValue,
40 | ): String? {
41 | if (valueArgument !is JsonPathFilterExpressionValue.ValueTypeValue.JsonValue) {
42 | return null
43 | }
44 |
45 | val valueElement = valueArgument.jsonElement
46 |
47 | if (valueElement !is JsonPrimitive || !valueElement.isString) {
48 | return null
49 | }
50 |
51 | return valueElement.content
52 | }
--------------------------------------------------------------------------------
/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/core/functionExtensions/searchFunctionExtension.kt:
--------------------------------------------------------------------------------
1 | package at.asitplus.jsonpath.core.functionExtensions
2 |
3 | import at.asitplus.jsonpath.core.JsonPathFilterExpressionType
4 | import at.asitplus.jsonpath.core.JsonPathFilterExpressionValue
5 | import at.asitplus.jsonpath.core.JsonPathFunctionExtension
6 | import kotlinx.serialization.json.JsonPrimitive
7 |
8 | /**
9 | * specification: https://datatracker.ietf.org/doc/rfc9535/
10 | * date: 2024-02
11 | * section: 2.4.7. search() Function Extension
12 | */
13 | internal val searchFunctionExtension by lazy {
14 | "search" to JsonPathFunctionExtension.LogicalTypeFunctionExtension(
15 | JsonPathFilterExpressionType.ValueType,
16 | JsonPathFilterExpressionType.ValueType,
17 | ) {
18 | val stringArgument = it[0] as JsonPathFilterExpressionValue.ValueTypeValue
19 | val regexArgument = it[1] as JsonPathFilterExpressionValue.ValueTypeValue
20 |
21 | val stringValue = unpackArgumentToStringValue(stringArgument)
22 | val regexValue = unpackArgumentToStringValue(regexArgument)
23 |
24 | if(stringValue == null || regexValue == null) {
25 | false
26 | } else {
27 | try {
28 | // TODO: check assumption that Regex supports RFC9485:
29 | // https://www.rfc-editor.org/rfc/rfc9485.html
30 | Regex(regexValue).containsMatchIn(stringValue)
31 | } catch (throwable: Throwable) {
32 | false
33 | }
34 | }
35 | }
36 | }
37 |
38 | private fun unpackArgumentToStringValue(
39 | valueArgument: JsonPathFilterExpressionValue.ValueTypeValue,
40 | ): String? {
41 | if (valueArgument !is JsonPathFilterExpressionValue.ValueTypeValue.JsonValue) {
42 | return null
43 | }
44 |
45 | val valueElement = valueArgument.jsonElement
46 |
47 | if (valueElement !is JsonPrimitive || !valueElement.isString) {
48 | return null
49 | }
50 |
51 | return valueElement.content
52 | }
--------------------------------------------------------------------------------
/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/core/functionExtensions/valueFunctionExtension.kt:
--------------------------------------------------------------------------------
1 | package at.asitplus.jsonpath.core.functionExtensions
2 |
3 | import at.asitplus.jsonpath.core.JsonPathFilterExpressionType
4 | import at.asitplus.jsonpath.core.JsonPathFilterExpressionValue
5 | import at.asitplus.jsonpath.core.JsonPathFunctionExtension
6 |
7 | /**
8 | * specification: https://datatracker.ietf.org/doc/rfc9535/
9 | * date: 2024-02
10 | * section: 2.4.8. value() Function Extension
11 | */
12 | internal val valueFunctionExtension by lazy {
13 | "value" to JsonPathFunctionExtension.ValueTypeFunctionExtension(
14 | JsonPathFilterExpressionType.NodesType,
15 | ) {
16 | val argument = it[0] as JsonPathFilterExpressionValue.NodesTypeValue
17 | if (argument.nodeList.size == 1) {
18 | argument.nodeList[0]
19 | } else null
20 | }
21 | }
--------------------------------------------------------------------------------
/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/implementation/AbstractSyntaxTree.kt:
--------------------------------------------------------------------------------
1 | package at.asitplus.jsonpath.implementation
2 |
3 | import org.antlr.v4.kotlinruntime.ParserRuleContext
4 | import org.antlr.v4.kotlinruntime.Token
5 | import org.antlr.v4.kotlinruntime.ast.Position
6 |
7 | internal data class AbstractSyntaxTree(
8 | val text: String,
9 | val position: Position?,
10 | val value: T,
11 | val children: List> = listOf(),
12 | ) {
13 | constructor(
14 | context: ParserRuleContext?,
15 | value: T,
16 | children: List> = listOf(),
17 | ) : this(
18 | text = context?.text ?: "",
19 | position = context?.position,
20 | value = value,
21 | children = children,
22 | )
23 | constructor(
24 | token: Token,
25 | value: T,
26 | children: List> = listOf(),
27 | ) : this(
28 | text = token.text ?: "",
29 | position = token.startPoint().let { Position(it, token.endPoint() ?: it) },
30 | value = value,
31 | children = children,
32 | )
33 | }
--------------------------------------------------------------------------------
/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/implementation/AntlrJsonPathCompiler.kt:
--------------------------------------------------------------------------------
1 | package at.asitplus.jsonpath.implementation
2 |
3 | import at.asitplus.jsonpath.core.JsonPathCompiler
4 | import at.asitplus.jsonpath.core.JsonPathFunctionExtension
5 | import at.asitplus.jsonpath.core.JsonPathQuery
6 | import at.asitplus.jsonpath.core.NodeList
7 | import at.asitplus.jsonpath.generated.JsonPathLexer
8 | import at.asitplus.jsonpath.generated.JsonPathParser
9 | import kotlinx.serialization.json.JsonElement
10 | import org.antlr.v4.kotlinruntime.CharStreams
11 | import org.antlr.v4.kotlinruntime.CommonTokenStream
12 | import org.antlr.v4.kotlinruntime.ListTokenSource
13 |
14 | class AntlrJsonPathCompiler(
15 | private var errorListener: AntlrJsonPathCompilerErrorListener? = null,
16 | ) : JsonPathCompiler {
17 | override fun compile(
18 | jsonPath: String,
19 | functionExtensionRetriever: (String) -> JsonPathFunctionExtension<*>?,
20 | ): JsonPathQuery {
21 | val lexerErrorDetector = AntlrSyntaxErrorDetector()
22 | val tokens = JsonPathLexer(CharStreams.fromString(jsonPath)).apply {
23 | addErrorListener(lexerErrorDetector)
24 | errorListener?.let {
25 | addErrorListener(it)
26 | }
27 | }.allTokens
28 |
29 | if (lexerErrorDetector.isError) {
30 | throw JsonPathLexerException()
31 | }
32 |
33 | val parserErrorDetector = AntlrSyntaxErrorDetector()
34 | val commonTokenStream = CommonTokenStream(ListTokenSource(tokens))
35 | val jsonPathQueryContext = JsonPathParser(commonTokenStream).apply {
36 | addErrorListener(parserErrorDetector)
37 | errorListener?.let {
38 | addErrorListener(it)
39 | }
40 | }.jsonpath_query()
41 |
42 | if (parserErrorDetector.isError) {
43 | throw JsonPathParserException()
44 | }
45 |
46 | val abstractSyntaxTree = AntlrJsonPathSemanticAnalyzerVisitor(
47 | errorListener = errorListener,
48 | functionExtensionRetriever = functionExtensionRetriever,
49 | ).visit(jsonPathQueryContext)
50 | val rootValueType = abstractSyntaxTree?.value
51 |
52 | if (rootValueType is JsonPathExpression.ErrorType) {
53 | throw JsonPathTypeCheckerException("Type errors have occured: $abstractSyntaxTree")
54 | }
55 | if (rootValueType !is JsonPathExpression.FilterExpression.NodesExpression.FilterQueryExpression) {
56 | throw JsonPathTypeCheckerException("Invalid root value type: $rootValueType: $abstractSyntaxTree")
57 | }
58 |
59 | return object : JsonPathQuery {
60 | override fun invoke(currentNode: JsonElement, rootNode: JsonElement): NodeList {
61 | return rootValueType.jsonPathQuery.invoke(
62 | currentNode = currentNode,
63 | rootNode = rootNode,
64 | )
65 | }
66 | }
67 | }
68 |
69 | fun setErrorListener(errorListener: AntlrJsonPathCompilerErrorListener?) {
70 | this.errorListener = errorListener
71 | }
72 |
73 | fun getErrorListener(): AntlrJsonPathCompilerErrorListener? {
74 | return errorListener
75 | }
76 | }
--------------------------------------------------------------------------------
/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/implementation/AntlrJsonPathCompilerErrorListener.kt:
--------------------------------------------------------------------------------
1 | package at.asitplus.jsonpath.implementation
2 |
3 | import org.antlr.v4.kotlinruntime.ANTLRErrorListener
4 |
5 | interface AntlrJsonPathCompilerErrorListener : AntlrJsonPathSemanticAnalyzerErrorListener, ANTLRErrorListener
--------------------------------------------------------------------------------
/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/implementation/AntlrJsonPathCompilerException.kt:
--------------------------------------------------------------------------------
1 | package at.asitplus.jsonpath.implementation
2 |
3 | import at.asitplus.jsonpath.core.JsonPathCompilerException
4 | import at.asitplus.jsonpath.core.JsonPathQueryException
5 | import at.asitplus.jsonpath.core.Rfc9535Utils
6 | import kotlinx.serialization.json.JsonObject
7 |
8 | /**
9 | * specification: https://datatracker.ietf.org/doc/rfc9535/
10 | * date: 2024-02
11 | */
12 | internal class JsonPathLexerException : JsonPathCompilerException(
13 | "Lexer errors have occured. See the output of the error listener for more details"
14 | )
15 |
16 | internal class JsonPathParserException : JsonPathCompilerException(
17 | "Parser errors have occured. See the output of the error listener for more details"
18 | )
19 |
20 | internal class JsonPathTypeCheckerException(message: String) : JsonPathCompilerException(message)
21 |
22 | internal class MissingKeyException(jsonObject: JsonObject, key: String) : JsonPathQueryException(
23 | "Missing key ${Rfc9535Utils.escapeToDoubleQuoted(key)} at object ${
24 | Rfc9535Utils.escapeToDoubleQuoted(
25 | jsonObject.toString()
26 | )
27 | }"
28 | )
--------------------------------------------------------------------------------
/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/implementation/AntlrJsonPathParserExtensions.kt:
--------------------------------------------------------------------------------
1 | package at.asitplus.jsonpath.implementation
2 |
3 | import at.asitplus.jsonpath.core.Rfc9535Utils
4 | import at.asitplus.jsonpath.generated.JsonPathParser
5 |
6 | internal fun JsonPathParser.StringLiteralContext.toUnescapedString(): String {
7 | return Rfc9535Utils.unpackStringLiteral(this.text)
8 | }
9 |
10 |
--------------------------------------------------------------------------------
/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/implementation/AntlrJsonPathSemanticAnalyzerErrorListener.kt:
--------------------------------------------------------------------------------
1 | package at.asitplus.jsonpath.implementation
2 |
3 | import at.asitplus.jsonpath.core.JsonPathFilterExpressionType
4 | import at.asitplus.jsonpath.core.JsonPathFunctionExtension
5 |
6 | interface AntlrJsonPathSemanticAnalyzerErrorListener {
7 | fun unknownFunctionExtension(functionExtensionName: String)
8 |
9 | /**
10 | * specification: https://datatracker.ietf.org/doc/rfc9535/
11 | * date: 2024-02
12 | * section 2.4.3: Well-Typedness of Function Expressions
13 | */
14 | fun invalidFunctionExtensionForTestExpression(functionExtensionName: String)
15 |
16 | fun invalidFunctionExtensionForComparable(functionExtensionName: String)
17 |
18 | fun invalidArglistForFunctionExtension(
19 | functionExtensionName: String,
20 | functionExtensionImplementation: JsonPathFunctionExtension<*>,
21 | coercedArgumentTypes: List>
22 | )
23 |
24 | fun invalidTestExpression(
25 | testContextString: String
26 | )
27 | }
--------------------------------------------------------------------------------
/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/implementation/AntlrJsonPathSemanticAnalyzerVisitor.kt:
--------------------------------------------------------------------------------
1 | package at.asitplus.jsonpath.implementation
2 |
3 | import at.asitplus.jsonpath.core.FilterPredicate
4 | import at.asitplus.jsonpath.core.JsonPathFilterExpressionType
5 | import at.asitplus.jsonpath.core.JsonPathFilterExpressionValue
6 | import at.asitplus.jsonpath.core.JsonPathFunctionExtension
7 | import at.asitplus.jsonpath.core.JsonPathSelector
8 | import at.asitplus.jsonpath.core.JsonPathSelectorQuery
9 | import at.asitplus.jsonpath.generated.JsonPathParser
10 | import at.asitplus.jsonpath.generated.JsonPathParserBaseVisitor
11 | import kotlinx.serialization.json.JsonArray
12 | import kotlinx.serialization.json.JsonElement
13 | import kotlinx.serialization.json.JsonNull
14 | import kotlinx.serialization.json.JsonObject
15 | import kotlinx.serialization.json.JsonPrimitive
16 | import kotlinx.serialization.json.booleanOrNull
17 | import kotlinx.serialization.json.doubleOrNull
18 | import kotlinx.serialization.json.longOrNull
19 | import org.antlr.v4.kotlinruntime.ParserRuleContext
20 | import org.antlr.v4.kotlinruntime.tree.TerminalNode
21 |
22 | /**
23 | * specification: https://datatracker.ietf.org/doc/rfc9535/
24 | * date: 2024-02
25 | * section 2.4.3: Well-Typedness of Function Expressions
26 | *
27 | * This class builds an abstract syntax tree where the nodes contain the logic necessary to be evaluated against an input.
28 | */
29 | internal class AntlrJsonPathSemanticAnalyzerVisitor(
30 | private val errorListener: AntlrJsonPathSemanticAnalyzerErrorListener?,
31 | private val functionExtensionRetriever: (String) -> JsonPathFunctionExtension<*>?,
32 | ) : JsonPathParserBaseVisitor>() {
33 | override fun defaultResult(): AbstractSyntaxTree {
34 | return AbstractSyntaxTree(context = null, value = JsonPathExpression.NoType)
35 | }
36 |
37 | override fun visitTerminal(node: TerminalNode): AbstractSyntaxTree {
38 | return AbstractSyntaxTree(token = node.symbol, value = JsonPathExpression.NoType)
39 | }
40 |
41 | override fun aggregateResult(
42 | aggregate: AbstractSyntaxTree,
43 | nextResult: AbstractSyntaxTree
44 | ): AbstractSyntaxTree {
45 | val children = (aggregate.children) + nextResult
46 | return AbstractSyntaxTree(
47 | context = null,
48 | value = if (children.any { it.value is JsonPathExpression.ErrorType }) {
49 | JsonPathExpression.ErrorType
50 | } else {
51 | when (aggregate.value) {
52 | is JsonPathExpression.ErrorType -> {
53 | JsonPathExpression.ErrorType
54 | }
55 |
56 | is JsonPathExpression.NoType -> {
57 | nextResult.value
58 | }
59 |
60 | else -> if (nextResult.value is JsonPathExpression.NoType) {
61 | // don't override a value with no value
62 | aggregate.value
63 | } else {
64 | // this is not generalizable anyway and needs to be handled for each node explicitly
65 | nextResult.value
66 | }
67 | }
68 | },
69 | children = children
70 | )
71 | }
72 |
73 | // queries
74 | override fun visitJsonpath_query(ctx: JsonPathParser.Jsonpath_queryContext): AbstractSyntaxTree {
75 | return QueryNodeBuilder(
76 | context = ctx,
77 | contextSelectorNode = visitRootIdentifier(ctx.rootIdentifier()),
78 | selectorSegmentTrees = ctx.segments().segment().map { visitSegment(it) }
79 | ).build()
80 | }
81 |
82 | override fun visitRel_query(ctx: JsonPathParser.Rel_queryContext): AbstractSyntaxTree {
83 | return QueryNodeBuilder(
84 | context = ctx,
85 | contextSelectorNode = visitCurrentNodeIdentifier(ctx.currentNodeIdentifier()),
86 | selectorSegmentTrees = ctx.segments().segment().map { visitSegment(it) }
87 | ).build()
88 | }
89 |
90 | override fun visitAbs_singular_query(ctx: JsonPathParser.Abs_singular_queryContext): AbstractSyntaxTree {
91 | return QueryNodeBuilder(
92 | context = ctx,
93 | contextSelectorNode = visitRootIdentifier(ctx.rootIdentifier()),
94 | selectorSegmentTrees = ctx.singular_query_segments().singular_query_segment().map {
95 | visitSingular_query_segment(it)
96 | },
97 | ).build()
98 | }
99 |
100 | override fun visitRel_singular_query(ctx: JsonPathParser.Rel_singular_queryContext): AbstractSyntaxTree {
101 | return QueryNodeBuilder(
102 | context = ctx,
103 | contextSelectorNode = visitCurrentNodeIdentifier(ctx.currentNodeIdentifier()),
104 | selectorSegmentTrees = ctx.singular_query_segments().singular_query_segment().map {
105 | visitSingular_query_segment(it)
106 | },
107 | ).build()
108 | }
109 |
110 | // selectors
111 | override fun visitRootIdentifier(ctx: JsonPathParser.RootIdentifierContext): AbstractSyntaxTree {
112 | return AbstractSyntaxTree(
113 | context = ctx,
114 | value = JsonPathExpression.SelectorExpression(JsonPathSelector.RootSelector)
115 | )
116 | }
117 |
118 | override fun visitCurrentNodeIdentifier(ctx: JsonPathParser.CurrentNodeIdentifierContext): AbstractSyntaxTree {
119 | return AbstractSyntaxTree(
120 | context = ctx,
121 | value = JsonPathExpression.SelectorExpression(JsonPathSelector.CurrentNodeSelector),
122 | )
123 | }
124 |
125 | override fun visitMemberNameShorthand(ctx: JsonPathParser.MemberNameShorthandContext): AbstractSyntaxTree {
126 | return AbstractSyntaxTree(
127 | context = ctx,
128 | value = JsonPathExpression.SelectorExpression(
129 | JsonPathSelector.MemberSelector(ctx.MEMBER_NAME_SHORTHAND().text),
130 | ),
131 | )
132 | }
133 |
134 | override fun visitName_selector(ctx: JsonPathParser.Name_selectorContext): AbstractSyntaxTree {
135 | return AbstractSyntaxTree(
136 | context = ctx,
137 | value = JsonPathExpression.SelectorExpression(
138 | JsonPathSelector.MemberSelector(
139 | ctx.stringLiteral().toUnescapedString()
140 | )
141 | )
142 | )
143 | }
144 |
145 | override fun visitIndex_selector(ctx: JsonPathParser.Index_selectorContext): AbstractSyntaxTree {
146 | return AbstractSyntaxTree(
147 | context = ctx,
148 | value = JsonPathExpression.SelectorExpression(
149 | JsonPathSelector.IndexSelector(
150 | ctx.int_expression().INT_TOKEN().text.toInt()
151 | )
152 | )
153 | )
154 | }
155 |
156 | override fun visitSlice_selector(ctx: JsonPathParser.Slice_selectorContext): AbstractSyntaxTree {
157 | return AbstractSyntaxTree(
158 | context = ctx,
159 | value = JsonPathExpression.SelectorExpression(
160 | JsonPathSelector.SliceSelector(
161 | startInclusive = ctx.start()?.text?.toInt(),
162 | endExclusive = ctx.end()?.text?.toInt(),
163 | step = ctx.step()?.text?.toInt(),
164 | )
165 | )
166 | )
167 | }
168 |
169 | override fun visitWildcardSelector(ctx: JsonPathParser.WildcardSelectorContext): AbstractSyntaxTree {
170 | return AbstractSyntaxTree(
171 | context = ctx,
172 | value = JsonPathExpression.SelectorExpression(
173 | JsonPathSelector.WildCardSelector
174 | )
175 | )
176 | }
177 |
178 | override fun visitDescendant_segment(ctx: JsonPathParser.Descendant_segmentContext): AbstractSyntaxTree {
179 | val child = ctx.bracketed_selection()?.let { visitBracketed_selection(it) }
180 | ?: ctx.memberNameShorthand()?.let { visitMemberNameShorthand(it) }
181 | ?: ctx.wildcardSelector()?.let { visitWildcardSelector(it) }
182 |
183 | val childValue = child?.value
184 |
185 | return AbstractSyntaxTree(
186 | context = ctx,
187 | value = if (childValue is JsonPathExpression.SelectorExpression) {
188 | JsonPathExpression.SelectorExpression(
189 | JsonPathSelector.DescendantSelector(
190 | childValue.selector
191 | )
192 | )
193 | } else JsonPathExpression.ErrorType,
194 | children = listOfNotNull(child)
195 | )
196 | }
197 |
198 | override fun visitBracketed_selection(ctx: JsonPathParser.Bracketed_selectionContext): AbstractSyntaxTree {
199 | val children = ctx.selector().map {
200 | visitSelector(it)
201 | }
202 |
203 | val selectorExpressionChildren = children.map {
204 | it.value
205 | }.filterIsInstance()
206 |
207 | return AbstractSyntaxTree(
208 | context = ctx,
209 | value = if (selectorExpressionChildren.size == children.size) {
210 | JsonPathExpression.SelectorExpression(
211 | JsonPathSelector.BracketedSelector(
212 | selectorExpressionChildren.map { it.selector }
213 | )
214 | )
215 | } else JsonPathExpression.ErrorType,
216 | children = children
217 | )
218 | }
219 |
220 | override fun visitFilter_selector(ctx: JsonPathParser.Filter_selectorContext): AbstractSyntaxTree {
221 | val logicalExpressionNode = visitLogical_expr(ctx.logical_expr())
222 | return AbstractSyntaxTree(
223 | context = ctx,
224 | value = if (logicalExpressionNode.value is JsonPathExpression.FilterExpression.LogicalExpression) {
225 | JsonPathExpression.SelectorExpression(
226 | JsonPathSelector.FilterSelector(
227 | object : FilterPredicate {
228 | override fun invoke(
229 | currentNode: JsonElement,
230 | rootNode: JsonElement
231 | ): Boolean = logicalExpressionNode.value.evaluate(
232 | JsonPathExpressionEvaluationContext(
233 | currentNode = currentNode,
234 | rootNode = rootNode,
235 | )
236 | ).isTrue
237 | }
238 | )
239 | )
240 | } else JsonPathExpression.ErrorType,
241 | children = listOf(logicalExpressionNode)
242 | )
243 | }
244 |
245 | // logical expressions
246 | override fun visitLogical_or_expr(ctx: JsonPathParser.Logical_or_exprContext): AbstractSyntaxTree {
247 | val children = ctx.logical_and_expr().map {
248 | visitLogical_and_expr(it)
249 | }
250 | val logicalChildrenValues = children.map { it.value }
251 | .filterIsInstance()
252 |
253 | return AbstractSyntaxTree(
254 | context = ctx,
255 | value = if (logicalChildrenValues.size == children.size) {
256 | JsonPathExpression.FilterExpression.LogicalExpression { context ->
257 | JsonPathFilterExpressionValue.LogicalTypeValue(
258 | logicalChildrenValues.any {
259 | it.evaluate(context).isTrue
260 | }
261 | )
262 | }
263 | } else JsonPathExpression.ErrorType,
264 | children = children,
265 | )
266 | }
267 |
268 | override fun visitLogical_and_expr(ctx: JsonPathParser.Logical_and_exprContext): AbstractSyntaxTree {
269 | val children = ctx.basic_expr().map {
270 | visitBasic_expr(it)
271 | }
272 | val logicalChildrenValues = children.map { it.value }
273 | .filterIsInstance()
274 |
275 | return AbstractSyntaxTree(
276 | context = ctx,
277 | value = if (logicalChildrenValues.size == children.size) {
278 | JsonPathExpression.FilterExpression.LogicalExpression { context ->
279 | JsonPathFilterExpressionValue.LogicalTypeValue(
280 | logicalChildrenValues.all {
281 | it.evaluate(context).isTrue
282 | }
283 | )
284 | }
285 | } else JsonPathExpression.ErrorType,
286 | children = children,
287 | )
288 | }
289 |
290 | override fun visitParen_expr(ctx: JsonPathParser.Paren_exprContext): AbstractSyntaxTree {
291 | val isNotNegated = ctx.LOGICAL_NOT_OP()?.let { false } ?: true
292 | val child = visitLogical_expr(ctx.logical_expr())
293 | return AbstractSyntaxTree(
294 | context = ctx,
295 | value = if (child.value is JsonPathExpression.FilterExpression.LogicalExpression) {
296 | JsonPathExpression.FilterExpression.LogicalExpression { context ->
297 | JsonPathFilterExpressionValue.LogicalTypeValue(
298 | child.value.evaluate(context).isTrue == isNotNegated
299 | )
300 | }
301 | } else JsonPathExpression.ErrorType,
302 | children = listOf(child),
303 | )
304 | }
305 |
306 | override fun visitTest_expr(ctx: JsonPathParser.Test_exprContext): AbstractSyntaxTree {
307 | val isNotNegated = (ctx.LOGICAL_NOT_OP() == null)
308 | return ctx.filter_query()?.let {
309 | val filterQueryTree = visitFilter_query(it)
310 | val filterQueryValue = filterQueryTree.value
311 | AbstractSyntaxTree(
312 | context = ctx,
313 | value = if (filterQueryValue is JsonPathExpression.FilterExpression.NodesExpression.FilterQueryExpression) {
314 | JsonPathExpression.FilterExpression.LogicalExpression { context ->
315 | JsonPathFilterExpressionValue.LogicalTypeValue(
316 | filterQueryValue.jsonPathQuery.invoke(
317 | currentNode = context.currentNode,
318 | rootNode = context.rootNode
319 | ).isNotEmpty() == isNotNegated
320 | )
321 | }
322 | } else JsonPathExpression.ErrorType,
323 | children = listOf(filterQueryTree)
324 | )
325 | } ?: ctx.function_expr()?.let { functionExpressionContext ->
326 | /**
327 | * specification: https://datatracker.ietf.org/doc/rfc9535/
328 | * date: 2024-02
329 | * section 2.4.3: Well-Typedness of Function Expressions
330 | *
331 | * As a test-expr in a logical expression:
332 | * The function's declared result type is LogicalType or (giving
333 | * rise to conversion as per Section 2.4.2) NodesType.
334 | */
335 | val child = visitFunction_expr(functionExpressionContext)
336 | val functionResultValue = child.value
337 | AbstractSyntaxTree(
338 | context = ctx,
339 | value = when (functionResultValue) {
340 | is JsonPathExpression.FilterExpression.ValueExpression -> {
341 | JsonPathExpression.ErrorType.also {
342 | errorListener?.invalidFunctionExtensionForTestExpression(
343 | functionExpressionContext.FUNCTION_NAME().text,
344 | )
345 | }
346 | }
347 |
348 | is JsonPathExpression.FilterExpression.LogicalExpression -> {
349 | JsonPathExpression.FilterExpression.LogicalExpression { context ->
350 | JsonPathFilterExpressionValue.LogicalTypeValue(
351 | functionResultValue.evaluate(context).isTrue == isNotNegated
352 | )
353 | }
354 | }
355 |
356 | is JsonPathExpression.FilterExpression.NodesExpression.NodesFunctionExpression -> {
357 | JsonPathExpression.FilterExpression.LogicalExpression { context ->
358 | JsonPathFilterExpressionValue.LogicalTypeValue(
359 | functionResultValue.evaluate(context).nodeList.isNotEmpty() == isNotNegated
360 | )
361 | }
362 | }
363 |
364 | else -> JsonPathExpression.ErrorType
365 | },
366 | children = listOf(child),
367 | )
368 | } ?: AbstractSyntaxTree(
369 | context = ctx,
370 | value = JsonPathExpression.ErrorType,
371 | ).also {
372 | errorListener?.invalidTestExpression(ctx.text)
373 | }
374 | }
375 |
376 | override fun visitFunction_expr(ctx: JsonPathParser.Function_exprContext): AbstractSyntaxTree {
377 | val functionArgumentNodes = ctx.function_argument().map {
378 | visitFunction_argument(it)
379 | }
380 |
381 | val extension = functionExtensionRetriever.invoke(ctx.FUNCTION_NAME().text)
382 | ?: return AbstractSyntaxTree(
383 | context = ctx,
384 | value = JsonPathExpression.ErrorType,
385 | children = functionArgumentNodes,
386 | ).also {
387 | errorListener?.unknownFunctionExtension(ctx.FUNCTION_NAME().text)
388 | }
389 |
390 | val isArglistSizeConsistent = ctx.function_argument().size == extension.argumentTypes.size
391 | val coercedArgumentExpressions =
392 | functionArgumentNodes.map { it.value }.mapIndexed { index, argumentNode ->
393 | when (extension.argumentTypes.getOrNull(index)) {
394 | /**
395 | * specification: https://datatracker.ietf.org/doc/rfc9535/
396 | * date: 2024-02
397 | * section 2.4.3: Well-Typedness of Function Expressions
398 | *
399 | * * When the declared type of the parameter is LogicalType and the
400 | * argument is one of the following:
401 | *
402 | * - A function expression with declared result type NodesType.
403 | * In this case, the argument is converted to LogicalType as
404 | * per Section 2.4.2.
405 | */
406 | JsonPathFilterExpressionType.LogicalType -> when (argumentNode) {
407 | is JsonPathExpression.FilterExpression.NodesExpression -> {
408 | JsonPathExpression.FilterExpression.LogicalExpression {
409 | JsonPathFilterExpressionValue.LogicalTypeValue(
410 | argumentNode.evaluate(it).nodeList.isNotEmpty()
411 | )
412 | }
413 | }
414 |
415 | else -> argumentNode
416 | }
417 |
418 | JsonPathFilterExpressionType.NodesType -> argumentNode
419 |
420 | /**
421 | * specification: https://datatracker.ietf.org/doc/rfc9535/
422 | * date: 2024-02
423 | * section 2.4.3: Well-Typedness of Function Expressions
424 | *
425 | * * When the declared type of the parameter is ValueType and the
426 | * argument is one of the following:
427 | *
428 | * - A value expressed as a literal.
429 | *
430 | * - A singular query. In this case:
431 | *
432 | * o If the query results in a nodelist consisting of a
433 | * single node, the argument is the value of the node.
434 | *
435 | * o If the query results in an empty nodelist, the argument
436 | * is the special result Nothing.
437 | */
438 | JsonPathFilterExpressionType.ValueType -> when (argumentNode) {
439 | is JsonPathExpression.FilterExpression.NodesExpression.FilterQueryExpression.SingularQueryExpression -> {
440 | argumentNode.toValueTypeValue()
441 | }
442 |
443 | else -> argumentNode
444 | }
445 |
446 | null -> argumentNode
447 | }
448 | }
449 |
450 | val coercedArgumentTypes = coercedArgumentExpressions.map {
451 | if (it !is JsonPathExpression.FilterExpression) {
452 | null
453 | } else {
454 | it.expressionType
455 | }
456 | }
457 |
458 | val isCoercedArgumentTypesMatching =
459 | coercedArgumentTypes.mapIndexed { index, argumentType ->
460 | argumentType == extension.argumentTypes[index]
461 | }.all {
462 | it
463 | }
464 |
465 | val isValidFunctionCall =
466 | isArglistSizeConsistent and isCoercedArgumentTypesMatching
467 |
468 | if (isValidFunctionCall == false) {
469 | errorListener?.invalidArglistForFunctionExtension(
470 | functionExtensionName = ctx.FUNCTION_NAME().text,
471 | functionExtensionImplementation = extension,
472 | coercedArgumentTypes = coercedArgumentTypes.zip(
473 | functionArgumentNodes.map {
474 | it.text
475 | }
476 | )
477 | )
478 | }
479 |
480 | return AbstractSyntaxTree(
481 | context = ctx,
482 | value = if (isValidFunctionCall) {
483 | val coercedArguments =
484 | coercedArgumentExpressions.filterIsInstance()
485 |
486 | when (extension) {
487 | is JsonPathFunctionExtension.LogicalTypeFunctionExtension -> {
488 | JsonPathExpression.FilterExpression.LogicalExpression { context ->
489 | extension.evaluate(coercedArguments.map {
490 | it.evaluate(context)
491 | })
492 | }
493 | }
494 |
495 | is JsonPathFunctionExtension.NodesTypeFunctionExtension -> {
496 | JsonPathExpression.FilterExpression.NodesExpression.NodesFunctionExpression { context ->
497 | extension.evaluate(coercedArguments.map {
498 | it.evaluate(context)
499 | })
500 | }
501 | }
502 |
503 | is JsonPathFunctionExtension.ValueTypeFunctionExtension -> {
504 | JsonPathExpression.FilterExpression.ValueExpression { context ->
505 | extension.evaluate(coercedArguments.map {
506 | it.evaluate(context)
507 | })
508 | }
509 | }
510 | }
511 | } else {
512 | JsonPathExpression.ErrorType
513 | },
514 | children = functionArgumentNodes,
515 | )
516 | }
517 |
518 | override fun visitComparison_expr(ctx: JsonPathParser.Comparison_exprContext): AbstractSyntaxTree {
519 | val firstComparable = visitComparable(ctx.firstComparable().comparable())
520 | val secondComparable = visitComparable(ctx.secondComparable().comparable())
521 | val children = listOf(firstComparable, secondComparable)
522 |
523 | val firstValue =
524 | if (firstComparable.value is JsonPathExpression.FilterExpression.NodesExpression.FilterQueryExpression.SingularQueryExpression) {
525 | firstComparable.value.toValueTypeValue()
526 | } else firstComparable.value
527 |
528 | val secondValue =
529 | if (secondComparable.value is JsonPathExpression.FilterExpression.NodesExpression.FilterQueryExpression.SingularQueryExpression) {
530 | secondComparable.value.toValueTypeValue()
531 | } else secondComparable.value
532 |
533 | listOf(
534 | ctx.firstComparable().comparable() to firstValue,
535 | ctx.secondComparable().comparable() to secondValue,
536 | ).forEach { (comparableContext, value) ->
537 | val functionExpressionContext = comparableContext.function_expr()
538 | when {
539 | value is JsonPathExpression.ErrorType -> {}
540 | functionExpressionContext != null -> {
541 | /**
542 | * specification: https://datatracker.ietf.org/doc/rfc9535/
543 | * date: 2024-02
544 | * section 2.4.3: Well-Typedness of Function Expressions
545 | *
546 | * As a comparable in a comparison:
547 | * The function's declared result type is ValueType.
548 | */
549 | if (value !is JsonPathExpression.FilterExpression.ValueExpression) {
550 | errorListener?.invalidFunctionExtensionForComparable(
551 | functionExpressionContext.FUNCTION_NAME().text,
552 | )
553 | }
554 | }
555 | }
556 | }
557 |
558 | return AbstractSyntaxTree(
559 | context = ctx,
560 | value = if (firstValue !is JsonPathExpression.FilterExpression.ValueExpression) {
561 | JsonPathExpression.ErrorType
562 | } else if (secondValue !is JsonPathExpression.FilterExpression.ValueExpression) {
563 | JsonPathExpression.ErrorType
564 | } else comparisonExpression(
565 | firstComparable = firstValue.evaluate,
566 | secondComparable = secondValue.evaluate,
567 | ctx.comparisonOp(),
568 | ),
569 | children = children,
570 | )
571 | }
572 |
573 | private fun comparisonExpression(
574 | firstComparable: (JsonPathExpressionEvaluationContext) -> JsonPathFilterExpressionValue.ValueTypeValue,
575 | secondComparable: (JsonPathExpressionEvaluationContext) -> JsonPathFilterExpressionValue.ValueTypeValue,
576 | comparisonOpContext: JsonPathParser.ComparisonOpContext,
577 | ): JsonPathExpression = comparisonOpContext.let {
578 | when {
579 | it.COMPARISON_OP_EQUALS() != null -> JsonPathExpression.FilterExpression.LogicalExpression { context ->
580 | JsonPathFilterExpressionValue.LogicalTypeValue(
581 | this.evaluateComparisonEquals(
582 | firstComparable.invoke(context),
583 | secondComparable.invoke(context),
584 | )
585 | )
586 | }
587 |
588 | it.COMPARISON_OP_SMALLER_THAN() != null -> JsonPathExpression.FilterExpression.LogicalExpression { context ->
589 | JsonPathFilterExpressionValue.LogicalTypeValue(
590 | evaluateComparisonSmallerThan(
591 | firstComparable.invoke(context),
592 | secondComparable.invoke(context),
593 | )
594 | )
595 | }
596 |
597 | it.COMPARISON_OP_NOT_EQUALS() != null -> JsonPathExpression.FilterExpression.LogicalExpression { context ->
598 | JsonPathFilterExpressionValue.LogicalTypeValue(
599 | !this.evaluateComparisonEquals(
600 | firstComparable.invoke(context),
601 | secondComparable.invoke(context),
602 | )
603 | )
604 | }
605 |
606 | it.COMPARISON_OP_SMALLER_THAN_OR_EQUALS() != null -> JsonPathExpression.FilterExpression.LogicalExpression { context ->
607 | JsonPathFilterExpressionValue.LogicalTypeValue(
608 | evaluateComparisonSmallerThan(
609 | firstComparable.invoke(context),
610 | secondComparable.invoke(context),
611 | ) or this.evaluateComparisonEquals(
612 | firstComparable.invoke(context),
613 | secondComparable.invoke(context),
614 | )
615 | )
616 | }
617 |
618 | it.COMPARISON_OP_GREATER_THAN() != null -> JsonPathExpression.FilterExpression.LogicalExpression { context ->
619 | JsonPathFilterExpressionValue.LogicalTypeValue(
620 | evaluateComparisonSmallerThan(
621 | secondComparable.invoke(context),
622 | firstComparable.invoke(context),
623 | )
624 | )
625 | }
626 |
627 | it.COMPARISON_OP_GREATER_THAN_OR_EQUALS() != null -> JsonPathExpression.FilterExpression.LogicalExpression { context ->
628 | JsonPathFilterExpressionValue.LogicalTypeValue(
629 | evaluateComparisonSmallerThan(
630 | secondComparable.invoke(context),
631 | firstComparable.invoke(context),
632 | ) or this.evaluateComparisonEquals(
633 | firstComparable.invoke(context),
634 | secondComparable.invoke(context),
635 | )
636 | )
637 | }
638 |
639 | else -> JsonPathExpression.ErrorType
640 | }
641 | }
642 |
643 | private fun evaluateComparisonEquals(
644 | firstValue: JsonPathFilterExpressionValue.ValueTypeValue,
645 | secondValue: JsonPathFilterExpressionValue.ValueTypeValue,
646 | ): Boolean {
647 | if (firstValue is JsonPathFilterExpressionValue.ValueTypeValue.Nothing) {
648 | return secondValue is JsonPathFilterExpressionValue.ValueTypeValue.Nothing
649 | }
650 | if (secondValue is JsonPathFilterExpressionValue.ValueTypeValue.Nothing) {
651 | return false
652 | }
653 |
654 | return evaluateComparisonEqualsUnpacked(
655 | firstValue,
656 | secondValue,
657 | )
658 | }
659 |
660 | private fun evaluateComparisonEqualsUnpacked(
661 | first: JsonPathFilterExpressionValue.ValueTypeValue,
662 | second: JsonPathFilterExpressionValue.ValueTypeValue,
663 | ): Boolean = when (first) {
664 | is JsonPathFilterExpressionValue.ValueTypeValue.JsonValue -> {
665 | if (second !is JsonPathFilterExpressionValue.ValueTypeValue.JsonValue) {
666 | false
667 | } else when (first.jsonElement) {
668 | JsonNull -> {
669 | second.jsonElement == JsonNull
670 | }
671 |
672 | is JsonPrimitive -> {
673 | if (second.jsonElement is JsonPrimitive) {
674 | when {
675 | first.jsonElement.isString != second.jsonElement.isString -> false
676 | first.jsonElement.isString -> first.jsonElement.content == second.jsonElement.content
677 | else -> first.jsonElement.booleanOrNull?.let { it == second.jsonElement.booleanOrNull }
678 | ?: first.jsonElement.longOrNull?.let { it == second.jsonElement.longOrNull }
679 | ?: first.jsonElement.doubleOrNull?.let { it == second.jsonElement.doubleOrNull }
680 | ?: false
681 | }
682 | } else false
683 | }
684 |
685 | is JsonArray -> {
686 | if (second.jsonElement is JsonArray) {
687 | (first.jsonElement.size == second.jsonElement.size) and first.jsonElement.mapIndexed { index, it ->
688 | index to it
689 | }.all {
690 | this.evaluateComparisonEqualsUnpacked(
691 | JsonPathFilterExpressionValue.ValueTypeValue.JsonValue(it.second),
692 | JsonPathFilterExpressionValue.ValueTypeValue.JsonValue(second.jsonElement[it.first]),
693 | )
694 | }
695 | } else false
696 | }
697 |
698 | is JsonObject -> {
699 | if (second.jsonElement is JsonObject) {
700 | (first.jsonElement.keys == second.jsonElement.keys) and first.jsonElement.entries.all {
701 | this.evaluateComparisonEqualsUnpacked(
702 | JsonPathFilterExpressionValue.ValueTypeValue.JsonValue(it.value),
703 | JsonPathFilterExpressionValue.ValueTypeValue.JsonValue(
704 | second.jsonElement[it.key]
705 | ?: throw MissingKeyException(
706 | jsonObject = second.jsonElement,
707 | key = it.key
708 | )
709 | )
710 | )
711 | }
712 | } else false
713 | }
714 | }
715 | }
716 |
717 | JsonPathFilterExpressionValue.ValueTypeValue.Nothing -> second == JsonPathFilterExpressionValue.ValueTypeValue.Nothing
718 | }
719 |
720 | private fun evaluateComparisonSmallerThan(
721 | firstValue: JsonPathFilterExpressionValue.ValueTypeValue,
722 | secondValue: JsonPathFilterExpressionValue.ValueTypeValue,
723 | ): Boolean {
724 | if (firstValue is JsonPathFilterExpressionValue.ValueTypeValue.Nothing) {
725 | return false
726 | }
727 | if (secondValue is JsonPathFilterExpressionValue.ValueTypeValue.Nothing) {
728 | return false
729 | }
730 |
731 | return evaluateComparisonUnpackedSmallerThan(
732 | firstValue,
733 | secondValue,
734 | )
735 | }
736 |
737 | private fun evaluateComparisonUnpackedSmallerThan(
738 | first: JsonPathFilterExpressionValue,
739 | second: JsonPathFilterExpressionValue,
740 | ): Boolean {
741 | if (first !is JsonPathFilterExpressionValue.ValueTypeValue.JsonValue) {
742 | return false
743 | }
744 | if (second !is JsonPathFilterExpressionValue.ValueTypeValue.JsonValue) {
745 | return false
746 | }
747 | if (first.jsonElement !is JsonPrimitive) {
748 | return false
749 | }
750 | if (second.jsonElement !is JsonPrimitive) {
751 | return false
752 | }
753 | if (first.jsonElement.isString != second.jsonElement.isString) {
754 | return false
755 | }
756 | if (first.jsonElement.isString) {
757 | return first.jsonElement.content < second.jsonElement.content
758 | }
759 | return first.jsonElement.longOrNull?.let { firstValue ->
760 | second.jsonElement.longOrNull?.let { firstValue < it }
761 | ?: second.jsonElement.doubleOrNull?.let { firstValue < it }
762 | } ?: first.jsonElement.doubleOrNull?.let { firstValue ->
763 | second.jsonElement.longOrNull?.let { firstValue < it }
764 | ?: second.jsonElement.doubleOrNull?.let { firstValue < it }
765 | } ?: false
766 | }
767 |
768 | // primitives
769 | override fun visitStringLiteral(ctx: JsonPathParser.StringLiteralContext): AbstractSyntaxTree {
770 | return AbstractSyntaxTree(
771 | context = ctx,
772 | value = JsonPathExpression.FilterExpression.ValueExpression {
773 | JsonPathFilterExpressionValue.ValueTypeValue.JsonValue(
774 | JsonPrimitive(ctx.toUnescapedString())
775 | )
776 | },
777 | )
778 | }
779 |
780 | override fun visitNumber_expression(ctx: JsonPathParser.Number_expressionContext): AbstractSyntaxTree {
781 | return AbstractSyntaxTree(
782 | context = ctx,
783 | value = JsonPathExpression.FilterExpression.ValueExpression {
784 | JsonPathFilterExpressionValue.ValueTypeValue.JsonValue(
785 | JsonPrimitive(ctx.NUMBER_TOKEN().text.toDouble())
786 | )
787 | },
788 | )
789 | }
790 |
791 | override fun visitInt_expression(ctx: JsonPathParser.Int_expressionContext): AbstractSyntaxTree {
792 | return AbstractSyntaxTree(
793 | context = ctx,
794 | value = JsonPathExpression.FilterExpression.ValueExpression {
795 | JsonPathFilterExpressionValue.ValueTypeValue.JsonValue(
796 | JsonPrimitive(ctx.INT_TOKEN().text.toInt())
797 | )
798 | },
799 | )
800 | }
801 |
802 | override fun visitTrue_expression(ctx: JsonPathParser.True_expressionContext): AbstractSyntaxTree {
803 | return AbstractSyntaxTree(
804 | context = ctx,
805 | value = JsonPathExpression.FilterExpression.ValueExpression {
806 | JsonPathFilterExpressionValue.ValueTypeValue.JsonValue(
807 | JsonPrimitive(true)
808 | )
809 | },
810 | )
811 | }
812 |
813 | override fun visitFalse_expression(ctx: JsonPathParser.False_expressionContext): AbstractSyntaxTree {
814 | return AbstractSyntaxTree(
815 | context = ctx,
816 | value = JsonPathExpression.FilterExpression.ValueExpression {
817 | JsonPathFilterExpressionValue.ValueTypeValue.JsonValue(
818 | JsonPrimitive(false)
819 | )
820 | },
821 | )
822 | }
823 |
824 | override fun visitNull_expression(ctx: JsonPathParser.Null_expressionContext): AbstractSyntaxTree {
825 | return AbstractSyntaxTree(
826 | context = ctx,
827 | value = JsonPathExpression.FilterExpression.ValueExpression {
828 | JsonPathFilterExpressionValue.ValueTypeValue.JsonValue(
829 | JsonNull
830 | )
831 | },
832 | )
833 | }
834 | }
835 |
836 |
837 | internal class QueryNodeBuilder(
838 | private val context: ParserRuleContext,
839 | private val contextSelectorNode: AbstractSyntaxTree,
840 | private val selectorSegmentTrees: List>
841 | ) {
842 | fun build(): AbstractSyntaxTree {
843 | val children = listOf(
844 | contextSelectorNode
845 | ) + selectorSegmentTrees
846 |
847 | val childrenValues = children.map { it.value }
848 | val childrenSelectors =
849 | childrenValues.filterIsInstance()
850 | val value = if (childrenValues.size != childrenSelectors.size) {
851 | JsonPathExpression.ErrorType
852 | } else {
853 | val query = JsonPathSelectorQuery(childrenSelectors.map { it.selector })
854 | if (query.isSingularQuery) {
855 | JsonPathExpression.FilterExpression.NodesExpression.FilterQueryExpression.SingularQueryExpression(
856 | query
857 | )
858 | } else {
859 | JsonPathExpression.FilterExpression.NodesExpression.FilterQueryExpression.NonSingularQueryExpression(
860 | query
861 | )
862 | }
863 | }
864 |
865 | return AbstractSyntaxTree(
866 | context = context,
867 | value = value,
868 | children = children,
869 | )
870 | }
871 | }
--------------------------------------------------------------------------------
/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/implementation/AntlrSyntaxErrorDetector.kt:
--------------------------------------------------------------------------------
1 | package at.asitplus.jsonpath.implementation
2 |
3 | import org.antlr.v4.kotlinruntime.BaseErrorListener
4 | import org.antlr.v4.kotlinruntime.RecognitionException
5 | import org.antlr.v4.kotlinruntime.Recognizer
6 |
7 | internal class AntlrSyntaxErrorDetector : BaseErrorListener() {
8 | var isError: Boolean = false
9 |
10 | override fun syntaxError(
11 | recognizer: Recognizer<*, *>,
12 | offendingSymbol: Any?,
13 | line: Int,
14 | charPositionInLine: Int,
15 | msg: String,
16 | e: RecognitionException?
17 | ) {
18 | isError = true
19 | }
20 | }
--------------------------------------------------------------------------------
/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/implementation/JsonPathExpression.kt:
--------------------------------------------------------------------------------
1 | package at.asitplus.jsonpath.implementation
2 |
3 | import at.asitplus.jsonpath.core.JsonPathFilterExpressionType
4 | import at.asitplus.jsonpath.core.JsonPathFilterExpressionValue
5 | import at.asitplus.jsonpath.core.JsonPathQuery
6 | import at.asitplus.jsonpath.core.JsonPathSelector
7 |
8 |
9 | internal sealed interface JsonPathExpression {
10 |
11 | sealed class FilterExpression(
12 | val expressionType: JsonPathFilterExpressionType,
13 | open val evaluate: (JsonPathExpressionEvaluationContext) -> JsonPathFilterExpressionValue,
14 | ) : JsonPathExpression {
15 | data class ValueExpression(
16 | override val evaluate: (JsonPathExpressionEvaluationContext) -> JsonPathFilterExpressionValue.ValueTypeValue
17 | ) : FilterExpression(
18 | expressionType = JsonPathFilterExpressionType.ValueType,
19 | evaluate = evaluate
20 | )
21 |
22 | data class LogicalExpression(
23 | override val evaluate: (JsonPathExpressionEvaluationContext) -> JsonPathFilterExpressionValue.LogicalTypeValue
24 | ) : FilterExpression(
25 | expressionType = JsonPathFilterExpressionType.LogicalType,
26 | evaluate = evaluate
27 | )
28 |
29 | sealed class NodesExpression(
30 | override val evaluate: (JsonPathExpressionEvaluationContext) -> JsonPathFilterExpressionValue.NodesTypeValue
31 | ) : FilterExpression(
32 | expressionType = JsonPathFilterExpressionType.NodesType,
33 | evaluate = evaluate
34 | ) {
35 | sealed class FilterQueryExpression(
36 | open val jsonPathQuery: JsonPathQuery,
37 | override val evaluate: (JsonPathExpressionEvaluationContext) -> JsonPathFilterExpressionValue.NodesTypeValue.FilterQueryResult
38 | ) : NodesExpression(evaluate) {
39 | data class SingularQueryExpression(
40 | override val jsonPathQuery: JsonPathQuery,
41 | override val evaluate: (JsonPathExpressionEvaluationContext) -> JsonPathFilterExpressionValue.NodesTypeValue.FilterQueryResult.SingularQueryResult = {
42 | val nodeList = jsonPathQuery.invoke(
43 | currentNode = it.currentNode,
44 | rootNode = it.rootNode,
45 | ).map {
46 | it.value
47 | }
48 | JsonPathFilterExpressionValue.NodesTypeValue.FilterQueryResult.SingularQueryResult(
49 | nodeList
50 | )
51 | }
52 | ) : FilterQueryExpression(
53 | jsonPathQuery = jsonPathQuery,
54 | evaluate = evaluate,
55 | ) {
56 | fun toValueTypeValue(): ValueExpression {
57 | return ValueExpression { context ->
58 | this.evaluate(context).nodeList.firstOrNull()?.let {
59 | JsonPathFilterExpressionValue.ValueTypeValue.JsonValue(it)
60 | } ?: JsonPathFilterExpressionValue.ValueTypeValue.Nothing
61 | }
62 | }
63 | }
64 |
65 | data class NonSingularQueryExpression(
66 | override val jsonPathQuery: JsonPathQuery,
67 | override val evaluate: (JsonPathExpressionEvaluationContext) -> JsonPathFilterExpressionValue.NodesTypeValue.FilterQueryResult.NonSingularQueryResult = {
68 | val nodeList = jsonPathQuery.invoke(
69 | currentNode = it.currentNode,
70 | rootNode = it.rootNode,
71 | ).map {
72 | it.value
73 | }
74 | JsonPathFilterExpressionValue.NodesTypeValue.FilterQueryResult.NonSingularQueryResult(
75 | nodeList
76 | )
77 | }
78 | ) : FilterQueryExpression(
79 | jsonPathQuery = jsonPathQuery,
80 | evaluate = evaluate
81 | )
82 | }
83 |
84 | data class NodesFunctionExpression(
85 | override val evaluate: (JsonPathExpressionEvaluationContext) -> JsonPathFilterExpressionValue.NodesTypeValue.FunctionExtensionResult
86 | ) : NodesExpression(evaluate)
87 | }
88 | }
89 |
90 | data class SelectorExpression(val selector: JsonPathSelector) : JsonPathExpression
91 |
92 | data object NoType : JsonPathExpression
93 | data object ErrorType : JsonPathExpression
94 | }
--------------------------------------------------------------------------------
/jsonpath4k/src/commonMain/kotlin/at/asitplus/jsonpath/implementation/JsonPathExpressionEvaluationContext.kt:
--------------------------------------------------------------------------------
1 | package at.asitplus.jsonpath.implementation
2 |
3 | import kotlinx.serialization.json.JsonElement
4 |
5 | internal data class JsonPathExpressionEvaluationContext(
6 | val currentNode: JsonElement,
7 | val rootNode: JsonElement = currentNode,
8 | )
--------------------------------------------------------------------------------
/jsonpath4k/src/commonMain/resources/grammar/JsonPath.abnf:
--------------------------------------------------------------------------------
1 | ; specification: https://datatracker.ietf.org/doc/rfc9535/
2 | ; date: 2024-02
3 | ; - Appendix A
4 |
5 | jsonpath-query = root-identifier segments
6 | segments = *(S segment)
7 |
8 | B = %x20 / ; Space
9 | %x09 / ; Horizontal tab
10 | %x0A / ; Line feed or New line
11 | %x0D ; Carriage return
12 | S = *B ; optional blank space
13 | root-identifier = "$"
14 | selector = name-selector /
15 | wildcard-selector /
16 | slice-selector /
17 | index-selector /
18 | filter-selector
19 | name-selector = string-literal
20 |
21 | string-literal = %x22 *double-quoted %x22 / ; "string"
22 | %x27 *single-quoted %x27 ; 'string'
23 |
24 | double-quoted = unescaped /
25 | %x27 / ; '
26 | ESC %x22 / ; \"
27 | ESC escapable
28 |
29 | single-quoted = unescaped /
30 | %x22 / ; "
31 | ESC %x27 / ; \'
32 | ESC escapable
33 |
34 | ESC = %x5C ; \ backslash
35 |
36 | unescaped = %x20-21 / ; see RFC 8259
37 | ; omit 0x22 "
38 | %x23-26 /
39 | ; omit 0x27 '
40 | %x28-5B /
41 | ; omit 0x5C \
42 | %x5D-D7FF /
43 | ; skip surrogate code points
44 | %xE000-10FFFF
45 |
46 | escapable = %x62 / ; b BS backspace U+0008
47 | %x66 / ; f FF form feed U+000C
48 | %x6E / ; n LF line feed U+000A
49 | %x72 / ; r CR carriage return U+000D
50 | %x74 / ; t HT horizontal tab U+0009
51 | "/" / ; / slash (solidus) U+002F
52 | "\" / ; \ backslash (reverse solidus) U+005C
53 | (%x75 hexchar) ; uXXXX U+XXXX
54 |
55 | hexchar = non-surrogate /
56 | (high-surrogate "\" %x75 low-surrogate)
57 | non-surrogate = ((DIGIT / "A"/"B"/"C" / "E"/"F") 3HEXDIG) /
58 | ("D" %x30-37 2HEXDIG )
59 | high-surrogate = "D" ("8"/"9"/"A"/"B") 2HEXDIG
60 | low-surrogate = "D" ("C"/"D"/"E"/"F") 2HEXDIG
61 |
62 | HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
63 | wildcard-selector = "*"
64 | index-selector = int ; decimal integer
65 |
66 | int = "0" /
67 | (["-"] DIGIT1 *DIGIT) ; - optional
68 | DIGIT1 = %x31-39 ; 1-9 non-zero digit
69 | slice-selector = [start S] ":" S [end S] [":" [S step ]]
70 |
71 | start = int ; included in selection
72 | end = int ; not included in selection
73 | step = int ; default: 1
74 | filter-selector = "?" S logical-expr
75 | logical-expr = logical-or-expr
76 | logical-or-expr = logical-and-expr *(S "||" S logical-and-expr)
77 | ; disjunction
78 | ; binds less tightly than conjunction
79 | logical-and-expr = basic-expr *(S "&&" S basic-expr)
80 | ; conjunction
81 | ; binds more tightly than disjunction
82 |
83 | basic-expr = paren-expr /
84 | comparison-expr /
85 | test-expr
86 |
87 | paren-expr = [logical-not-op S] "(" S logical-expr S ")"
88 | ; parenthesized expression
89 | logical-not-op = "!" ; logical NOT operator
90 | test-expr = [logical-not-op S]
91 | (filter-query / ; existence/non-existence
92 | function-expr) ; LogicalType or NodesType
93 | filter-query = rel-query / jsonpath-query
94 | rel-query = current-node-identifier segments
95 | current-node-identifier = "@"
96 | comparison-expr = comparable S comparison-op S comparable
97 | literal = number / string-literal /
98 | true / false / null
99 | comparable = literal /
100 | singular-query / ; singular query value
101 | function-expr ; ValueType
102 | comparison-op = "==" / "!=" /
103 | "<=" / ">=" /
104 | "<" / ">"
105 |
106 | singular-query = rel-singular-query / abs-singular-query
107 | rel-singular-query = current-node-identifier singular-query-segments
108 | abs-singular-query = root-identifier singular-query-segments
109 | singular-query-segments = *(S (name-segment / index-segment))
110 | name-segment = ("[" name-selector "]") /
111 | ("." member-name-shorthand)
112 | index-segment = "[" index-selector "]"
113 | number = (int / "-0") [ frac ] [ exp ] ; decimal number
114 | frac = "." 1*DIGIT ; decimal fraction
115 | exp = "e" [ "-" / "+" ] 1*DIGIT ; decimal exponent
116 | true = %x74.72.75.65 ; true
117 | false = %x66.61.6c.73.65 ; false
118 | null = %x6e.75.6c.6c ; null
119 | function-name = function-name-first *function-name-char
120 | function-name-first = LCALPHA
121 | function-name-char = function-name-first / "_" / DIGIT
122 | LCALPHA = %x61-7A ; "a".."z"
123 |
124 | function-expr = function-name "(" S [function-argument
125 | *(S "," S function-argument)] S ")"
126 | function-argument = literal /
127 | filter-query / ; (includes singular-query)
128 | logical-expr /
129 | function-expr
130 | segment = child-segment / descendant-segment
131 | child-segment = bracketed-selection /
132 | ("."
133 | (wildcard-selector /
134 | member-name-shorthand))
135 |
136 | bracketed-selection = "[" S selector *(S "," S selector) S "]"
137 |
138 | member-name-shorthand = name-first *name-char
139 | name-first = ALPHA /
140 | "_" /
141 | %x80-D7FF /
142 | ; skip surrogate code points
143 | %xE000-10FFFF
144 | name-char = name-first / DIGIT
145 |
146 | DIGIT = %x30-39 ; 0-9
147 | ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
148 | descendant-segment = ".." (bracketed-selection /
149 | wildcard-selector /
150 | member-name-shorthand)
151 |
--------------------------------------------------------------------------------
/jsonpath4k/src/commonMain/resources/grammar/JsonPathLexer.g4:
--------------------------------------------------------------------------------
1 | // 1. converted from abnf using tool:
2 | // - http://www.robertpinchbeck.com/abnf_to_antlr/Default.aspx
3 | // 2. manually resolved lexer ambiguities
4 | lexer grammar JsonPathLexer;
5 |
6 | // normal
7 | ROOT_IDENTIFIER: '$';
8 | CURRENT_NODE_IDENTIFIER : '@';
9 |
10 | BLANK: BLANK_FRAGMENT;
11 |
12 | DESCENDANT_SELECTOR: '..' -> pushMode(optionalShorthandMode);
13 | SHORTHAND_SELECTOR: '.' -> pushMode(optionalShorthandMode);
14 | WILDCARD_SELECTOR: WILDCARD_SELECTOR_FRAGMENT;
15 |
16 | COLON: ':';
17 | COMMA: ',';
18 | SQUARE_BRACKET_OPEN: SQUARE_BRACKET_OPEN_FRAGMENT;
19 | SQUARE_BRACKET_CLOSE: ']';
20 |
21 | QUESTIONMARK: '?';
22 | BRACKET_OPEN: '(';
23 | BRACKET_CLOSE: ')';
24 |
25 | LOGICAL_NOT_OP: '!'; // logical NOT operator
26 | LOGICAL_OR_OP: '||';
27 | LOGICAL_AND_OP: '&&';
28 |
29 | COMPARISON_OP_EQUALS : '==';
30 | COMPARISON_OP_NOT_EQUALS : '!=';
31 | COMPARISON_OP_SMALLER_THAN : '<';
32 | COMPARISON_OP_GREATER_THAN : '>';
33 | COMPARISON_OP_SMALLER_THAN_OR_EQUALS : '<=';
34 | COMPARISON_OP_GREATER_THAN_OR_EQUALS : '>=';
35 |
36 | STRING_LITERAL: STRING_LITERAL_FRAGMENT;
37 |
38 | NULL_TOKEN : 'null';
39 | TRUE_TOKEN : 'true';
40 | FALSE_TOKEN : 'false';
41 | INT_TOKEN: INT_FRAGMENT; // match before number, and just accept both for the number literal
42 | NUMBER_TOKEN: (INT_FRAGMENT | NEGATIVE_ZERO) DECIMAL_FRACTION? EXPONENT?;
43 |
44 | FUNCTION_NAME: FUNCTION_NAME_FRAGMENT;
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | fragment BLANK_FRAGMENT: '\u0020' | // Space
54 | '\u0009' | // Horizontal tab
55 | '\u000A' | // Line feed or New line
56 | '\u000D'; // Carriage return
57 |
58 | fragment ZERO: '\u0030';
59 | fragment DIGIT1: '\u0031'..'\u0039';
60 | fragment DIGIT: ZERO | DIGIT1;
61 | fragment INT_FRAGMENT: ZERO | (MINUS? DIGIT1 DIGIT*);
62 | fragment A : 'A' | 'a';
63 | fragment B : 'B' | 'b';
64 | fragment C : 'C' | 'c';
65 | fragment D : 'D' | 'd';
66 | fragment E : 'E' | 'e';
67 | fragment F : 'F' | 'f';
68 | fragment HEXDIGIT : DIGIT | A | B | C | D | E | F;
69 |
70 | fragment HIGH_SURROGATE : D ('8'|'9'|A|B) (HEXDIGIT HEXDIGIT);
71 | fragment LOW_SURROGATE : D (C|D|E|F) (HEXDIGIT HEXDIGIT);
72 | fragment NON_SURROGATE : ((DIGIT | A|B|C|E|F) (HEXDIGIT HEXDIGIT HEXDIGIT)) |
73 | (D '\u0030'..'\u0037' (HEXDIGIT HEXDIGIT) );
74 | fragment HEXCHAR : NON_SURROGATE |
75 | (HIGH_SURROGATE BACKSLASH 'u' LOW_SURROGATE);
76 |
77 | fragment LCALPHA : [a-z];
78 | fragment UCALPHA : [A-Z];
79 | fragment ALPHA : LCALPHA | UCALPHA;
80 |
81 | fragment WILDCARD_SELECTOR_FRAGMENT: '*';
82 | fragment SQUARE_BRACKET_OPEN_FRAGMENT: '[';
83 | fragment UNDERLINE: '_';
84 | fragment BACKSLASH: '\\';
85 | fragment PLUS: '+';
86 | fragment MINUS: '-';
87 |
88 | fragment NAME_FIRST: ALPHA |
89 | UNDERLINE |
90 | '\u0080'..'\uD7FF' |
91 | // skip surrogate code points
92 | '\uE000'..'\uFFFF';
93 | fragment NAME_CHAR : NAME_FIRST | DIGIT;
94 | fragment MEMBER_NAME_SHORTHAND_FRAGMENT: NAME_FIRST NAME_CHAR*;
95 |
96 | fragment ESCAPABLE: 'b' | // b BS backspace U+0008
97 | 'f' | // f FF form feed U+000C
98 | 'n' | // n LF line feed U+000A
99 | 'r' | // r CR carriage return U+000D
100 | 't' | // t HT horizontal tab U+0009
101 | '/' | // / slash (solidus) U+002F
102 | BACKSLASH | // \ backslash (reverse solidus) U+005C
103 | ('u' HEXCHAR); // uXXXX U+XXXX
104 |
105 | fragment UNESCAPED: '\u0020' | '\u0021' | // see RFC 8259
106 | // omit 0x22 "
107 | '\u0023'..'\u0026' |
108 | // omit 0x27 '
109 | '\u0028'..'\u005B' |
110 | // omit 0x5C \
111 | '\u005D'..'\uD7FF' |
112 | // skip surrogate code points
113 | '\uE000'..'\uFFFF'
114 | ;
115 |
116 | fragment ESC: BACKSLASH;
117 | fragment SQUOTE: '\'';
118 | fragment DQUOTE: '"';
119 | fragment DOUBLE_QUOTED : UNESCAPED |
120 | SQUOTE | // '
121 | (ESC DQUOTE) | // \"
122 | (ESC ESCAPABLE);
123 |
124 | fragment SINGLE_QUOTED : UNESCAPED |
125 | DQUOTE | // "
126 | (ESC SQUOTE) | // \'
127 | (ESC ESCAPABLE);
128 |
129 | // needs to be a single token in order to disabiguate escapable and unescaped characters
130 | // from ones in MEMBER_NAME_SHORTHAND or FUNCTION_NAME
131 | fragment STRING_LITERAL_FRAGMENT
132 | : (DQUOTE DOUBLE_QUOTED* DQUOTE) // "string"
133 | | (SQUOTE SINGLE_QUOTED* SQUOTE) // 'string'
134 | ;
135 |
136 | fragment FUNCTION_NAME_FIRST: LCALPHA;
137 | fragment FUNCTION_NAME_CHAR: FUNCTION_NAME_FIRST | UNDERLINE | DIGIT;
138 | fragment FUNCTION_NAME_FRAGMENT: FUNCTION_NAME_FIRST FUNCTION_NAME_CHAR*;
139 |
140 | fragment NEGATIVE_ZERO: MINUS ZERO;
141 | fragment INT_WITH_POSSIBLE_ZERO_PREFIX: DIGIT+;
142 | fragment DECIMAL_FRACTION: '.' INT_WITH_POSSIBLE_ZERO_PREFIX;
143 |
144 | fragment SIGN: (MINUS | PLUS);
145 | fragment EXPONENT: E SIGN? INT_WITH_POSSIBLE_ZERO_PREFIX; // decimal exponent
146 |
147 |
148 |
149 |
150 | mode optionalShorthandMode; // needed to disabiguate MEMBER_NAME_SHORTHAND from FUNCTION_NAME
151 | MEMBER_NAME_SHORTHAND: MEMBER_NAME_SHORTHAND_FRAGMENT -> popMode;
152 | WILDCARD_SELECTOR_1: WILDCARD_SELECTOR_FRAGMENT -> type(WILDCARD_SELECTOR), popMode;
153 | SQUARE_BRACKET_OPEN_1: SQUARE_BRACKET_OPEN_FRAGMENT -> type(SQUARE_BRACKET_OPEN), popMode;
--------------------------------------------------------------------------------
/jsonpath4k/src/commonMain/resources/grammar/JsonPathParser.g4:
--------------------------------------------------------------------------------
1 | // 1. converted from abnf using tool:
2 | // - http://www.robertpinchbeck.com/abnf_to_antlr/Default.aspx
3 | // 2. manually resolved lexer ambiguities
4 | parser grammar JsonPathParser;
5 |
6 | options { tokenVocab=JsonPathLexer; }
7 |
8 | jsonpath_query : rootIdentifier segments;
9 | segments : (ws segment)*;
10 | segment : bracketed_selection | SHORTHAND_SELECTOR shorthand_segment | DESCENDANT_SELECTOR descendant_segment;
11 | shorthand_segment : wildcardSelector | memberNameShorthand;
12 | descendant_segment : bracketed_selection |
13 | wildcardSelector |
14 | memberNameShorthand;
15 |
16 | bracketed_selection : SQUARE_BRACKET_OPEN ws selector (ws COMMA ws selector)* ws SQUARE_BRACKET_CLOSE;
17 | selector : name_selector |
18 | wildcardSelector |
19 | slice_selector |
20 | index_selector |
21 | filter_selector;
22 | name_selector : stringLiteral;
23 |
24 |
25 | index_selector : int_expression; // decimal integer
26 |
27 | slice_selector : (start ws)? COLON ws (end ws)? (COLON (ws step )?)?;
28 |
29 | start : int_expression; // included in selection
30 | end : int_expression; // not included in selection
31 | step : int_expression; // default: 1
32 |
33 | // used in filter, but executed in normal mode
34 | filter_query : rel_query | jsonpath_query;
35 | rel_query : currentNodeIdentifier segments;
36 |
37 | singular_query : rel_singular_query | abs_singular_query;
38 | rel_singular_query : currentNodeIdentifier singular_query_segments;
39 | abs_singular_query : rootIdentifier singular_query_segments;
40 | singular_query_segments : (ws (singular_query_segment))*;
41 | singular_query_segment : name_segment | index_segment;
42 | name_segment : (SQUARE_BRACKET_OPEN name_selector SQUARE_BRACKET_CLOSE) |
43 | (SHORTHAND_SELECTOR memberNameShorthand);
44 | index_segment : SQUARE_BRACKET_OPEN index_selector SQUARE_BRACKET_CLOSE;
45 |
46 |
47 | filter_selector : QUESTIONMARK ws logical_expr;
48 | logical_expr : logical_or_expr;
49 | logical_or_expr : logical_and_expr (ws LOGICAL_OR_OP ws logical_and_expr)*;
50 | // disjunction
51 | // binds less tightly than conjunction
52 | logical_and_expr : basic_expr (ws LOGICAL_AND_OP ws basic_expr)*;
53 | // conjunction
54 | // binds more tightly than disjunction
55 |
56 | basic_expr : paren_expr |
57 | comparison_expr |
58 | test_expr;
59 |
60 | paren_expr : (LOGICAL_NOT_OP ws)? BRACKET_OPEN ws logical_expr ws BRACKET_CLOSE;
61 | // parenthesized expression
62 | test_expr : (LOGICAL_NOT_OP ws)?
63 | (filter_query | // existence/non-existence
64 | function_expr); // LogicalType or NodesType
65 | comparison_expr : firstComparable ws comparisonOp ws secondComparable;
66 | firstComparable: comparable;
67 | secondComparable: comparable;
68 | literal : int_expression | number_expression | stringLiteral |
69 | true_expression | false_expression | null_expression;
70 | comparable : literal |
71 | singular_query | // singular query value
72 | function_expr; // ValueType
73 |
74 | function_expr : FUNCTION_NAME BRACKET_OPEN ws (function_argument
75 | (ws COMMA ws function_argument)*)? ws BRACKET_CLOSE;
76 | function_argument : literal |
77 | filter_query | // (includes singular-query)
78 | function_expr |
79 | logical_expr;
80 |
81 |
82 |
83 | rootIdentifier: ROOT_IDENTIFIER;
84 | currentNodeIdentifier: CURRENT_NODE_IDENTIFIER;
85 | ws: BLANK*;
86 |
87 | wildcardSelector: WILDCARD_SELECTOR;
88 | memberNameShorthand: MEMBER_NAME_SHORTHAND;
89 |
90 | stringLiteral: STRING_LITERAL;
91 | number_expression: NUMBER_TOKEN; // integer is matched before number, but it is also a valid number literal
92 | int_expression: INT_TOKEN; // integer is matched before number, but it is also a valid number literal
93 | true_expression: TRUE_TOKEN;
94 | false_expression: FALSE_TOKEN;
95 | null_expression: NULL_TOKEN;
96 |
97 | comparisonOp
98 | : COMPARISON_OP_EQUALS | COMPARISON_OP_NOT_EQUALS
99 | | COMPARISON_OP_SMALLER_THAN | COMPARISON_OP_GREATER_THAN
100 | | COMPARISON_OP_SMALLER_THAN_OR_EQUALS | COMPARISON_OP_GREATER_THAN_OR_EQUALS
101 | ;
102 |
103 |
--------------------------------------------------------------------------------
/jsonpath4k/src/commonTest/kotlin/KotestConfig.kt:
--------------------------------------------------------------------------------
1 | import io.github.aakira.napier.DebugAntilog
2 | import io.github.aakira.napier.Napier
3 | import io.kotest.core.config.AbstractProjectConfig
4 |
5 | object KotestConfig : AbstractProjectConfig() {
6 | init {
7 | Napier.base(DebugAntilog())
8 | }
9 | }
--------------------------------------------------------------------------------
/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/DependencyManagementTest.kt:
--------------------------------------------------------------------------------
1 | package at.asitplus.jsonpath
2 |
3 | import at.asitplus.jsonpath.core.JsonPathCompiler
4 | import at.asitplus.jsonpath.core.JsonPathFunctionExtension
5 | import at.asitplus.jsonpath.core.JsonPathQuery
6 | import at.asitplus.jsonpath.core.NodeList
7 | import io.kotest.assertions.throwables.shouldNotThrowAny
8 | import io.kotest.core.spec.style.FreeSpec
9 | import io.kotest.matchers.collections.shouldBeIn
10 | import io.kotest.matchers.collections.shouldHaveSize
11 | import io.kotest.matchers.nulls.shouldNotBeNull
12 | import kotlinx.serialization.json.JsonElement
13 | import kotlinx.serialization.json.JsonNull
14 | import kotlinx.serialization.json.buildJsonObject
15 |
16 | @Suppress("unused")
17 | class DependencyManagementTest : FreeSpec({
18 | // making sure that the dependencies are reset to their default for the next test
19 | val defaultCompilerBuilderBackup = JsonPathDependencyManager.compiler
20 | val defaultFunctionExtensionRepositoryBackup =
21 | JsonPathDependencyManager.functionExtensionRepository.export()
22 | beforeEach {
23 | // prepare a dummy repository to be modified by the tests
24 | JsonPathDependencyManager.functionExtensionRepository =
25 | JsonPathFunctionExtensionMapRepository(
26 | defaultFunctionExtensionRepositoryBackup.toMutableMap()
27 | )
28 | }
29 | afterEach {
30 | JsonPathDependencyManager.apply {
31 | compiler = defaultCompilerBuilderBackup
32 | functionExtensionRepository = JsonPathFunctionExtensionMapRepository(
33 | defaultFunctionExtensionRepositoryBackup.toMutableMap()
34 | )
35 | }
36 | }
37 |
38 | "dependency manager compiler should support the functions in the repo at the time of compilation, and query should be executable afterwards too" - {
39 | "compiler that was built when the repository supported a function extension before it was removed should succeed compilation before and query afterwards" {
40 | val jsonPathStatement = "$[?foo()]"
41 |
42 | val jsonPath = shouldNotThrowAny {
43 | val testRepo = JsonPathDependencyManager.functionExtensionRepository.export().plus(
44 | "foo" to JsonPathFunctionExtension.LogicalTypeFunctionExtension {
45 | true
46 | }
47 | )
48 | JsonPath(jsonPathStatement, functionExtensionRetriever = testRepo::get)
49 | }
50 |
51 | shouldNotThrowAny {
52 | val jsonElement = buildJsonObject {
53 | put("a", JsonNull)
54 | }
55 | val nodeList = jsonPath.query(jsonElement)
56 |
57 | nodeList shouldHaveSize 1
58 | jsonElement["a"].shouldNotBeNull().shouldBeIn(nodeList.map { it.value })
59 | }
60 | }
61 | }
62 |
63 | "changing the compiler also changes the compiler used in the next JsonPath" {
64 | val incorrectEmptyQueryCompiler = object : JsonPathCompiler {
65 | override fun compile(
66 | jsonPath: String,
67 | functionExtensionRetriever: (String) -> JsonPathFunctionExtension<*>?,
68 | ): JsonPathQuery {
69 | return object : JsonPathQuery {
70 | override fun invoke(currentNode: JsonElement, rootNode: JsonElement): NodeList {
71 | return listOf()
72 | }
73 | }
74 | }
75 | }
76 | JsonPathDependencyManager.compiler = incorrectEmptyQueryCompiler
77 | val emptyQueryResult = JsonPath("$").query(buildJsonObject {})
78 | // this checks, whether the new compiler has indeed been used
79 | emptyQueryResult.shouldHaveSize(0)
80 | }
81 | })
82 |
--------------------------------------------------------------------------------
/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/NodeListSerializationTest.kt:
--------------------------------------------------------------------------------
1 | package at.asitplus.jsonpath
2 |
3 | import at.asitplus.jsonpath.core.NodeListEntry
4 | import at.asitplus.jsonpath.core.NormalizedJsonPath
5 | import at.asitplus.jsonpath.core.NormalizedJsonPathSegment
6 | import io.kotest.core.spec.style.FreeSpec
7 | import io.kotest.matchers.shouldBe
8 | import kotlinx.serialization.encodeToString
9 | import kotlinx.serialization.json.Json
10 | import kotlinx.serialization.json.JsonPrimitive
11 | import kotlinx.serialization.json.buildJsonArray
12 | import kotlinx.serialization.json.buildJsonObject
13 |
14 | @Suppress("unused")
15 | class NodeListSerializationTest : FreeSpec({
16 | "NormalizedJsonPathSegment" - {
17 | "NameSegment" {
18 | val segment: NormalizedJsonPathSegment = NormalizedJsonPathSegment.NameSegment("test")
19 |
20 | val stringified = Json.encodeToString(segment)
21 | val reconstructed = Json.decodeFromString(stringified)
22 |
23 | reconstructed.toString() shouldBe segment.toString()
24 | }
25 | "IndexSegment" {
26 | val segment: NormalizedJsonPathSegment = NormalizedJsonPathSegment.IndexSegment(42u)
27 |
28 | val stringified = Json.encodeToString(segment)
29 | val reconstructed = Json.decodeFromString(stringified)
30 |
31 | reconstructed.toString() shouldBe segment.toString()
32 | }
33 | }
34 | "NodeListEntry" - {
35 | "1 NameSegment" {
36 | val jsonObject = buildJsonObject {
37 | put("test", JsonPrimitive(42))
38 | }
39 | val entry = NodeListEntry(
40 | normalizedJsonPath = NormalizedJsonPath(NormalizedJsonPathSegment.NameSegment("test")),
41 | value = jsonObject
42 | )
43 | val stringified = Json.encodeToString(entry)
44 | val reconstructed = Json.decodeFromString(stringified)
45 | reconstructed.normalizedJsonPath.toString() shouldBe entry.normalizedJsonPath.toString()
46 | }
47 | "1 IndexSegment" {
48 | val jsonObject = buildJsonArray {
49 | add(JsonPrimitive(42))
50 | }
51 | val entry = NodeListEntry(
52 | normalizedJsonPath = NormalizedJsonPath(NormalizedJsonPathSegment.IndexSegment(0u)),
53 | value = jsonObject
54 | )
55 | val stringified = Json.encodeToString(entry)
56 | val reconstructed = Json.decodeFromString(stringified)
57 | reconstructed.normalizedJsonPath.toString() shouldBe entry.normalizedJsonPath.toString()
58 | }
59 | "1 IndexSegment, 1 NameSegment" {
60 | val key = "key"
61 | val jsonObject = buildJsonArray {
62 | add(buildJsonObject {
63 | put(key, JsonPrimitive(42))
64 | })
65 | }
66 | val entry = NodeListEntry(
67 | normalizedJsonPath = NormalizedJsonPath(
68 | NormalizedJsonPathSegment.IndexSegment(0u),
69 | NormalizedJsonPathSegment.NameSegment(key),
70 | ),
71 | value = jsonObject
72 | )
73 | val stringified = Json.encodeToString(entry)
74 | val reconstructed = Json.decodeFromString(stringified)
75 | reconstructed.normalizedJsonPath.toString() shouldBe entry.normalizedJsonPath.toString()
76 | }
77 | "1 NameSegment, 1 IndexSegment" {
78 | val key = "key"
79 | val jsonObject = buildJsonObject {
80 | put(key, buildJsonArray {
81 | add(JsonPrimitive(42))
82 | })
83 | }
84 | val entry = NodeListEntry(
85 | normalizedJsonPath = NormalizedJsonPath(
86 | NormalizedJsonPathSegment.IndexSegment(0u),
87 | NormalizedJsonPathSegment.NameSegment(key),
88 | ),
89 | value = jsonObject
90 | )
91 | val stringified = Json.encodeToString(entry)
92 | val reconstructed = Json.decodeFromString(stringified)
93 | reconstructed.normalizedJsonPath.toString() shouldBe entry.normalizedJsonPath.toString()
94 | }
95 | }
96 | "NodeList" - {
97 | // trusting the default serializer for list for this one
98 | }
99 | })
--------------------------------------------------------------------------------
/jsonpath4k/src/commonTest/kotlin/at/asitplus/jsonpath/Rfc9535UtilsUnitTest.kt:
--------------------------------------------------------------------------------
1 | package at.asitplus.jsonpath
2 |
3 | import at.asitplus.jsonpath.core.Rfc9535Utils
4 | import io.kotest.core.spec.style.FreeSpec
5 | import io.kotest.matchers.shouldBe
6 |
7 | @Suppress("unused")
8 | class Rfc9535UtilsUnitTest : FreeSpec({
9 | "Rfc9535Utils.unpackStringLiteral Unit Tests" - {
10 | "rfc8259 conformance" - {
11 | "special escape characters" - {
12 | "double quoted" - {
13 | "unescapes backslash to backslash" {
14 | Rfc9535Utils.unpackStringLiteral("\"\\\\\"") shouldBe "\\"
15 | }
16 | "unescapes slash to slash" {
17 | Rfc9535Utils.unpackStringLiteral("\"\\/\"") shouldBe "/"
18 | }
19 | "unescapes quotation mark to quotation mark" {
20 | Rfc9535Utils.unpackStringLiteral("\"\\\"\"") shouldBe "\""
21 | }
22 | "unescapes b to backspace" {
23 | Rfc9535Utils.unpackStringLiteral("\"\\b\"") shouldBe Char(0x0008).toString()
24 | }
25 | "unescapes f to form feed" {
26 | Rfc9535Utils.unpackStringLiteral("\"\\f\"") shouldBe Char(0x000C).toString()
27 | }
28 | "unescapes n to newline" {
29 | Rfc9535Utils.unpackStringLiteral("\"\\n\"") shouldBe "\n"
30 | }
31 | "unescapes r to carriage return" {
32 | Rfc9535Utils.unpackStringLiteral("\"\\r\"") shouldBe "\r"
33 | }
34 | "unescapes t to horizontal tab" {
35 | Rfc9535Utils.unpackStringLiteral("\"\\t\"") shouldBe "\t"
36 | }
37 | }
38 | "single quoted" - {
39 | "unescapes backslash to backslash" {
40 | Rfc9535Utils.unpackStringLiteral("'\\\\'") shouldBe "\\"
41 | }
42 | "unescapes slash to slash" {
43 | Rfc9535Utils.unpackStringLiteral("'\\/'") shouldBe "/"
44 | }
45 | "unescapes quotation mark to quotation mark" {
46 | Rfc9535Utils.unpackStringLiteral("'\\''") shouldBe "'"
47 | }
48 | "unescapes b to backspace" {
49 | Rfc9535Utils.unpackStringLiteral("'\\b'") shouldBe Char(0x0008).toString()
50 | }
51 | "unescapes f to form feed" {
52 | Rfc9535Utils.unpackStringLiteral("'\\f'") shouldBe Char(0x000C).toString()
53 | }
54 | "unescapes n to newline" {
55 | Rfc9535Utils.unpackStringLiteral("'\\n'") shouldBe "\n"
56 | }
57 | "unescapes r to carriage return" {
58 | Rfc9535Utils.unpackStringLiteral("'\\r'") shouldBe "\r"
59 | }
60 | "unescapes t to horizontal tab" {
61 | Rfc9535Utils.unpackStringLiteral("'\\t'") shouldBe "\t"
62 | }
63 | }
64 | }
65 | }
66 | }
67 | "Rfc9535Utils.switchToSingleQuotedString Unit Tests" - {
68 | "rfc8259 conformance" - {
69 | "special escape characters" - {
70 | "\"\\\\\"" {
71 | Rfc9535Utils.switchToSingleQuotedString(this.testScope.testCase.name.originalName)
72 | .let {
73 | val expectedResult = "'\\\\'"
74 | it shouldBe expectedResult
75 | Rfc9535Utils.switchToSingleQuotedString(it) shouldBe expectedResult
76 | }
77 | }
78 | "\"\\/\"" {
79 | Rfc9535Utils.switchToSingleQuotedString(this.testScope.testCase.name.originalName)
80 | .let {
81 | val expectedResult = "'\\/'"
82 | it shouldBe expectedResult
83 | Rfc9535Utils.switchToSingleQuotedString(it) shouldBe expectedResult
84 | }
85 | }
86 | "\"\\\"\"" {
87 | Rfc9535Utils.switchToSingleQuotedString(this.testScope.testCase.name.originalName)
88 | .let {
89 | val expectedResult = "'\"'"
90 | it shouldBe expectedResult
91 | Rfc9535Utils.switchToSingleQuotedString(it) shouldBe expectedResult
92 | }
93 | }
94 | "\"'\"" {
95 | Rfc9535Utils.switchToSingleQuotedString(this.testScope.testCase.name.originalName)
96 | .let {
97 | val expectedResult = "'\\''"
98 | it shouldBe expectedResult
99 | Rfc9535Utils.switchToSingleQuotedString(it) shouldBe expectedResult
100 | }
101 | }
102 | "\"\\b\"" {
103 | Rfc9535Utils.switchToSingleQuotedString(this.testScope.testCase.name.originalName)
104 | .let {
105 | val expectedResult = "'\\b'"
106 | it shouldBe expectedResult
107 | Rfc9535Utils.switchToSingleQuotedString(it) shouldBe expectedResult
108 | }
109 | }
110 | "\"\\f\"" {
111 | Rfc9535Utils.switchToSingleQuotedString(this.testScope.testCase.name.originalName)
112 | .let {
113 | val expectedResult = "'\\f'"
114 | it shouldBe expectedResult
115 | Rfc9535Utils.switchToSingleQuotedString(it) shouldBe expectedResult
116 | }
117 | }
118 | "\"\\n\"" {
119 | Rfc9535Utils.switchToSingleQuotedString(this.testScope.testCase.name.originalName)
120 | .let {
121 | val expectedResult = "'\\n'"
122 | it shouldBe expectedResult
123 | Rfc9535Utils.switchToSingleQuotedString(it) shouldBe expectedResult
124 | }
125 | }
126 | "\"\\r\"" {
127 | Rfc9535Utils.switchToSingleQuotedString(this.testScope.testCase.name.originalName)
128 | .let {
129 | val expectedResult = "'\\r'"
130 | it shouldBe expectedResult
131 | Rfc9535Utils.switchToSingleQuotedString(it) shouldBe expectedResult
132 | }
133 | }
134 | "\"\\t\"" {
135 | Rfc9535Utils.switchToSingleQuotedString(this.testScope.testCase.name.originalName)
136 | .let {
137 | val expectedResult = "'\\t'"
138 | it shouldBe expectedResult
139 | Rfc9535Utils.switchToSingleQuotedString(it) shouldBe expectedResult
140 | }
141 | }
142 | }
143 | }
144 | }
145 | "Rfc9535Utils.switchToDoubleQuotedString Unit Tests" - {
146 | "rfc8259 conformance" - {
147 | "special escape characters" - {
148 | "'\\\\'" {
149 | Rfc9535Utils.switchToDoubleQuotedString(this.testScope.testCase.name.originalName)
150 | .let {
151 | val expectedResult = "\"\\\\\""
152 | it shouldBe expectedResult
153 | Rfc9535Utils.switchToDoubleQuotedString(it) shouldBe expectedResult
154 | }
155 | }
156 | "'\\/'" {
157 | Rfc9535Utils.switchToDoubleQuotedString(this.testScope.testCase.name.originalName)
158 | .let {
159 | val expectedResult = "\"\\/\""
160 | it shouldBe expectedResult
161 | Rfc9535Utils.switchToDoubleQuotedString(it) shouldBe expectedResult
162 | }
163 | }
164 | "'\"'" {
165 | Rfc9535Utils.switchToDoubleQuotedString(this.testScope.testCase.name.originalName)
166 | .let {
167 | val expectedResult = "\"\\\"\""
168 | it shouldBe expectedResult
169 | Rfc9535Utils.switchToDoubleQuotedString(it) shouldBe expectedResult
170 | }
171 | }
172 | "'\\''" {
173 | Rfc9535Utils.switchToDoubleQuotedString(this.testScope.testCase.name.originalName)
174 | .let {
175 | val expectedResult = "\"'\""
176 | it shouldBe expectedResult
177 | Rfc9535Utils.switchToDoubleQuotedString(it) shouldBe expectedResult
178 | }
179 | }
180 | "'\\b'" {
181 | Rfc9535Utils.switchToDoubleQuotedString(this.testScope.testCase.name.originalName)
182 | .let {
183 | val expectedResult = "\"\\b\""
184 | it shouldBe expectedResult
185 | Rfc9535Utils.switchToDoubleQuotedString(it) shouldBe expectedResult
186 | }
187 | }
188 | "'\\f'" {
189 | Rfc9535Utils.switchToDoubleQuotedString(this.testScope.testCase.name.originalName)
190 | .let {
191 | val expectedResult = "\"\\f\""
192 | it shouldBe expectedResult
193 | Rfc9535Utils.switchToDoubleQuotedString(it) shouldBe expectedResult
194 | }
195 | }
196 | "'\\n'" {
197 | Rfc9535Utils.switchToDoubleQuotedString(this.testScope.testCase.name.originalName)
198 | .let {
199 | val expectedResult = "\"\\n\""
200 | it shouldBe expectedResult
201 | Rfc9535Utils.switchToDoubleQuotedString(it) shouldBe expectedResult
202 | }
203 | }
204 | "'\\r'" {
205 | Rfc9535Utils.switchToDoubleQuotedString(this.testScope.testCase.name.originalName)
206 | .let {
207 | val expectedResult = "\"\\r\""
208 | it shouldBe expectedResult
209 | Rfc9535Utils.switchToDoubleQuotedString(it) shouldBe expectedResult
210 | }
211 | }
212 | "'\\t'" {
213 | Rfc9535Utils.switchToDoubleQuotedString(this.testScope.testCase.name.originalName)
214 | .let {
215 | val expectedResult = "\"\\t\""
216 | it shouldBe expectedResult
217 | Rfc9535Utils.switchToDoubleQuotedString(it) shouldBe expectedResult
218 | }
219 | }
220 | }
221 | }
222 | }
223 | })
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | rootProject.name = "JsonPath4K"
2 |
3 | pluginManagement {
4 | repositories {
5 | maven("https://s01.oss.sonatype.org/content/repositories/snapshots") //KOTEST snapshot
6 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
7 | google()
8 | gradlePluginPortal()
9 | mavenCentral()
10 | }
11 | }
12 |
13 | dependencyResolutionManagement {
14 | repositories {
15 | google()
16 | mavenCentral()
17 | mavenLocal()
18 | }
19 | }
20 |
21 | include(":jsonpath4k")
--------------------------------------------------------------------------------