├── .cargo └── config.toml ├── .editorconfig ├── .github ├── semantic.yml └── workflows │ └── ci.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── licenserc.toml ├── rust-toolchain.toml ├── rustfmt.toml ├── spath ├── Cargo.toml ├── src │ ├── json.rs │ ├── lib.rs │ ├── node.rs │ ├── parser │ │ ├── error.rs │ │ ├── input.rs │ │ ├── mod.rs │ │ ├── parse.rs │ │ ├── range.rs │ │ ├── runner.rs │ │ └── token.rs │ ├── path.rs │ ├── spath.rs │ ├── spec │ │ ├── function │ │ │ ├── builtin.rs │ │ │ ├── expr.rs │ │ │ ├── mod.rs │ │ │ ├── types.rs │ │ │ └── value.rs │ │ ├── mod.rs │ │ ├── query.rs │ │ ├── segment.rs │ │ └── selector │ │ │ ├── filter.rs │ │ │ ├── index.rs │ │ │ ├── mod.rs │ │ │ ├── name.rs │ │ │ └── slice.rs │ ├── toml.rs │ └── value.rs ├── testdata │ ├── learn-toml-in-y-minutes.toml │ ├── rfc-9535-example-1.json │ ├── rfc-9535-example-10.json │ ├── rfc-9535-example-2.json │ ├── rfc-9535-example-3.json │ ├── rfc-9535-example-4.json │ ├── rfc-9535-example-5.json │ ├── rfc-9535-example-6.json │ ├── rfc-9535-example-7.json │ ├── rfc-9535-example-8.json │ └── rfc-9535-example-9.json └── tests │ ├── common │ └── mod.rs │ ├── spec.rs │ └── toml.rs ├── taplo.toml ├── typos.toml └── xtask ├── Cargo.toml └── src └── main.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 tison 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | [alias] 16 | x = "run --package x --" 17 | 18 | [env] 19 | CARGO_WORKSPACE_DIR = { value = "", relative = true } 20 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | indent_style = space 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | 9 | [*.toml] 10 | indent_size = tab 11 | tab_width = 2 12 | -------------------------------------------------------------------------------- /.github/semantic.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 tison 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # The pull request's title should be fulfilled the following pattern: 16 | # 17 | # [optional scope]: 18 | # 19 | # ... where valid types and scopes can be found below; for example: 20 | # 21 | # build(maven): One level down for native profile 22 | # 23 | # More about configurations on https://github.com/Ezard/semantic-prs#configuration 24 | 25 | enabled: true 26 | 27 | titleOnly: true 28 | 29 | types: 30 | - feat 31 | - fix 32 | - docs 33 | - style 34 | - refactor 35 | - perf 36 | - test 37 | - build 38 | - ci 39 | - chore 40 | - revert 41 | 42 | targetUrl: https://github.com/cratesland/spath/blob/main/.github/semantic.yml 43 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 tison 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: CI 16 | on: 17 | pull_request: 18 | branches: [ main ] 19 | push: 20 | branches: [ main ] 21 | 22 | # Concurrency strategy: 23 | # github.workflow: distinguish this workflow from others 24 | # github.event_name: distinguish `push` event from `pull_request` event 25 | # github.event.number: set to the number of the pull request if `pull_request` event 26 | # github.run_id: otherwise, it's a `push` event, only cancel if we rerun the workflow 27 | # 28 | # Reference: 29 | # https://docs.github.com/en/actions/using-jobs/using-concurrency 30 | # https://docs.github.com/en/actions/learn-github-actions/contexts#github-context 31 | concurrency: 32 | group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.number || github.run_id }} 33 | cancel-in-progress: true 34 | 35 | jobs: 36 | check: 37 | name: Check 38 | runs-on: ubuntu-22.04 39 | steps: 40 | - uses: actions/checkout@v4 41 | - name: Install toolchain 42 | uses: dtolnay/rust-toolchain@nightly 43 | with: 44 | components: rustfmt,clippy 45 | - uses: Swatinem/rust-cache@v2 46 | - uses: taiki-e/install-action@v2 47 | with: 48 | tool: typos-cli,taplo-cli,hawkeye 49 | - run: cargo +nightly x lint 50 | 51 | test: 52 | name: Run tests 53 | strategy: 54 | matrix: 55 | os: [ ubuntu-22.04, macos-14, windows-2022 ] 56 | rust-version: [ "1.80.0", "stable" ] 57 | runs-on: ${{ matrix.os }} 58 | steps: 59 | - uses: actions/checkout@v4 60 | - uses: Swatinem/rust-cache@v2 61 | - name: Delete rust-toolchain.toml 62 | run: rm rust-toolchain.toml 63 | - name: Install toolchain 64 | uses: dtolnay/rust-toolchain@master 65 | with: 66 | toolchain: ${{ matrix.rust-version }} 67 | - name: Run unit tests 68 | run: cargo x test --no-capture 69 | shell: bash 70 | 71 | required: 72 | name: Required 73 | runs-on: ubuntu-22.04 74 | if: ${{ always() }} 75 | needs: 76 | - check 77 | - test 78 | steps: 79 | - name: Guardian 80 | run: | 81 | if [[ ! ( \ 82 | "${{ needs.check.result }}" == "success" \ 83 | && "${{ needs.test.result }}" == "success" \ 84 | ) ]]; then 85 | echo "Required jobs haven't been completed successfully." 86 | exit -1 87 | fi 88 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "annotate-snippets" 16 | version = "0.11.5" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "710e8eae58854cdc1790fcb56cca04d712a17be849eeb81da2a724bf4bae2bc4" 19 | dependencies = [ 20 | "anstyle", 21 | "unicode-width", 22 | ] 23 | 24 | [[package]] 25 | name = "anstream" 26 | version = "0.6.18" 27 | source = "registry+https://github.com/rust-lang/crates.io-index" 28 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 29 | dependencies = [ 30 | "anstyle", 31 | "anstyle-parse", 32 | "anstyle-query", 33 | "anstyle-wincon", 34 | "colorchoice", 35 | "is_terminal_polyfill", 36 | "utf8parse", 37 | ] 38 | 39 | [[package]] 40 | name = "anstyle" 41 | version = "1.0.10" 42 | source = "registry+https://github.com/rust-lang/crates.io-index" 43 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 44 | 45 | [[package]] 46 | name = "anstyle-parse" 47 | version = "0.2.6" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 50 | dependencies = [ 51 | "utf8parse", 52 | ] 53 | 54 | [[package]] 55 | name = "anstyle-query" 56 | version = "1.1.2" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 59 | dependencies = [ 60 | "windows-sys", 61 | ] 62 | 63 | [[package]] 64 | name = "anstyle-wincon" 65 | version = "3.0.7" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 68 | dependencies = [ 69 | "anstyle", 70 | "once_cell", 71 | "windows-sys", 72 | ] 73 | 74 | [[package]] 75 | name = "autocfg" 76 | version = "1.4.0" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 79 | 80 | [[package]] 81 | name = "beef" 82 | version = "0.5.2" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" 85 | 86 | [[package]] 87 | name = "bitflags" 88 | version = "2.8.0" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" 91 | 92 | [[package]] 93 | name = "clap" 94 | version = "4.5.29" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "8acebd8ad879283633b343856142139f2da2317c96b05b4dd6181c61e2480184" 97 | dependencies = [ 98 | "clap_builder", 99 | "clap_derive", 100 | ] 101 | 102 | [[package]] 103 | name = "clap_builder" 104 | version = "4.5.29" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "f6ba32cbda51c7e1dfd49acc1457ba1a7dec5b64fe360e828acb13ca8dc9c2f9" 107 | dependencies = [ 108 | "anstream", 109 | "anstyle", 110 | "clap_lex", 111 | "strsim", 112 | ] 113 | 114 | [[package]] 115 | name = "clap_derive" 116 | version = "4.5.28" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" 119 | dependencies = [ 120 | "heck", 121 | "proc-macro2", 122 | "quote", 123 | "syn", 124 | ] 125 | 126 | [[package]] 127 | name = "clap_lex" 128 | version = "0.7.4" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 131 | 132 | [[package]] 133 | name = "colorchoice" 134 | version = "1.0.3" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 137 | 138 | [[package]] 139 | name = "console" 140 | version = "0.15.10" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "ea3c6ecd8059b57859df5c69830340ed3c41d30e3da0c1cbed90a96ac853041b" 143 | dependencies = [ 144 | "encode_unicode", 145 | "libc", 146 | "once_cell", 147 | "windows-sys", 148 | ] 149 | 150 | [[package]] 151 | name = "either" 152 | version = "1.13.0" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 155 | 156 | [[package]] 157 | name = "encode_unicode" 158 | version = "1.0.0" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" 161 | 162 | [[package]] 163 | name = "env_home" 164 | version = "0.1.0" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" 167 | 168 | [[package]] 169 | name = "equivalent" 170 | version = "1.0.1" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 173 | 174 | [[package]] 175 | name = "errno" 176 | version = "0.3.10" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 179 | dependencies = [ 180 | "libc", 181 | "windows-sys", 182 | ] 183 | 184 | [[package]] 185 | name = "fnv" 186 | version = "1.0.7" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 189 | 190 | [[package]] 191 | name = "googletest" 192 | version = "0.13.0" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "dce026f84cdd339bf71be01b24fe67470ee634282f68c1c4b563d00a9f002b05" 195 | dependencies = [ 196 | "googletest_macro", 197 | "num-traits", 198 | "regex", 199 | "rustversion", 200 | ] 201 | 202 | [[package]] 203 | name = "googletest_macro" 204 | version = "0.13.0" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "f5070fa86976044fe2b004d874c10af5d1aed6d8f6a72ff93a6eb29cc87048bc" 207 | dependencies = [ 208 | "proc-macro2", 209 | "quote", 210 | "syn", 211 | ] 212 | 213 | [[package]] 214 | name = "hashbrown" 215 | version = "0.15.2" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 218 | 219 | [[package]] 220 | name = "heck" 221 | version = "0.5.0" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 224 | 225 | [[package]] 226 | name = "indexmap" 227 | version = "2.7.1" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" 230 | dependencies = [ 231 | "equivalent", 232 | "hashbrown", 233 | ] 234 | 235 | [[package]] 236 | name = "insta" 237 | version = "1.42.1" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "71c1b125e30d93896b365e156c33dadfffab45ee8400afcbba4752f59de08a86" 240 | dependencies = [ 241 | "console", 242 | "linked-hash-map", 243 | "once_cell", 244 | "pin-project", 245 | "serde", 246 | "similar", 247 | ] 248 | 249 | [[package]] 250 | name = "is_terminal_polyfill" 251 | version = "1.70.1" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 254 | 255 | [[package]] 256 | name = "itoa" 257 | version = "1.0.14" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" 260 | 261 | [[package]] 262 | name = "lazy_static" 263 | version = "1.5.0" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 266 | 267 | [[package]] 268 | name = "libc" 269 | version = "0.2.169" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 272 | 273 | [[package]] 274 | name = "linked-hash-map" 275 | version = "0.5.6" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" 278 | 279 | [[package]] 280 | name = "linux-raw-sys" 281 | version = "0.4.15" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 284 | 285 | [[package]] 286 | name = "logos" 287 | version = "0.15.0" 288 | source = "registry+https://github.com/rust-lang/crates.io-index" 289 | checksum = "ab6f536c1af4c7cc81edf73da1f8029896e7e1e16a219ef09b184e76a296f3db" 290 | dependencies = [ 291 | "logos-derive", 292 | ] 293 | 294 | [[package]] 295 | name = "logos-codegen" 296 | version = "0.15.0" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "189bbfd0b61330abea797e5e9276408f2edbe4f822d7ad08685d67419aafb34e" 299 | dependencies = [ 300 | "beef", 301 | "fnv", 302 | "lazy_static", 303 | "proc-macro2", 304 | "quote", 305 | "regex-syntax", 306 | "rustc_version", 307 | "syn", 308 | ] 309 | 310 | [[package]] 311 | name = "logos-derive" 312 | version = "0.15.0" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "ebfe8e1a19049ddbfccbd14ac834b215e11b85b90bab0c2dba7c7b92fb5d5cba" 315 | dependencies = [ 316 | "logos-codegen", 317 | ] 318 | 319 | [[package]] 320 | name = "memchr" 321 | version = "2.7.4" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 324 | 325 | [[package]] 326 | name = "num-cmp" 327 | version = "0.1.0" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" 330 | 331 | [[package]] 332 | name = "num-traits" 333 | version = "0.2.19" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 336 | dependencies = [ 337 | "autocfg", 338 | ] 339 | 340 | [[package]] 341 | name = "once_cell" 342 | version = "1.20.3" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" 345 | 346 | [[package]] 347 | name = "pin-project" 348 | version = "1.1.9" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "dfe2e71e1471fe07709406bf725f710b02927c9c54b2b5b2ec0e8087d97c327d" 351 | dependencies = [ 352 | "pin-project-internal", 353 | ] 354 | 355 | [[package]] 356 | name = "pin-project-internal" 357 | version = "1.1.9" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "f6e859e6e5bd50440ab63c47e3ebabc90f26251f7c73c3d3e837b74a1cc3fa67" 360 | dependencies = [ 361 | "proc-macro2", 362 | "quote", 363 | "syn", 364 | ] 365 | 366 | [[package]] 367 | name = "proc-macro2" 368 | version = "1.0.93" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" 371 | dependencies = [ 372 | "unicode-ident", 373 | ] 374 | 375 | [[package]] 376 | name = "quote" 377 | version = "1.0.38" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 380 | dependencies = [ 381 | "proc-macro2", 382 | ] 383 | 384 | [[package]] 385 | name = "regex" 386 | version = "1.11.1" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 389 | dependencies = [ 390 | "aho-corasick", 391 | "memchr", 392 | "regex-automata", 393 | "regex-syntax", 394 | ] 395 | 396 | [[package]] 397 | name = "regex-automata" 398 | version = "0.4.9" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 401 | dependencies = [ 402 | "aho-corasick", 403 | "memchr", 404 | "regex-syntax", 405 | ] 406 | 407 | [[package]] 408 | name = "regex-syntax" 409 | version = "0.8.5" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 412 | 413 | [[package]] 414 | name = "rustc_version" 415 | version = "0.4.1" 416 | source = "registry+https://github.com/rust-lang/crates.io-index" 417 | checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" 418 | dependencies = [ 419 | "semver", 420 | ] 421 | 422 | [[package]] 423 | name = "rustix" 424 | version = "0.38.44" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" 427 | dependencies = [ 428 | "bitflags", 429 | "errno", 430 | "libc", 431 | "linux-raw-sys", 432 | "windows-sys", 433 | ] 434 | 435 | [[package]] 436 | name = "rustversion" 437 | version = "1.0.19" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" 440 | 441 | [[package]] 442 | name = "ryu" 443 | version = "1.0.19" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" 446 | 447 | [[package]] 448 | name = "semver" 449 | version = "1.0.25" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" 452 | 453 | [[package]] 454 | name = "serde" 455 | version = "1.0.217" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" 458 | dependencies = [ 459 | "serde_derive", 460 | ] 461 | 462 | [[package]] 463 | name = "serde_derive" 464 | version = "1.0.217" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" 467 | dependencies = [ 468 | "proc-macro2", 469 | "quote", 470 | "syn", 471 | ] 472 | 473 | [[package]] 474 | name = "serde_json" 475 | version = "1.0.138" 476 | source = "registry+https://github.com/rust-lang/crates.io-index" 477 | checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" 478 | dependencies = [ 479 | "itoa", 480 | "memchr", 481 | "ryu", 482 | "serde", 483 | ] 484 | 485 | [[package]] 486 | name = "serde_spanned" 487 | version = "0.6.8" 488 | source = "registry+https://github.com/rust-lang/crates.io-index" 489 | checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" 490 | dependencies = [ 491 | "serde", 492 | ] 493 | 494 | [[package]] 495 | name = "similar" 496 | version = "2.7.0" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" 499 | 500 | [[package]] 501 | name = "spath" 502 | version = "0.3.1" 503 | dependencies = [ 504 | "annotate-snippets", 505 | "googletest", 506 | "insta", 507 | "logos", 508 | "num-cmp", 509 | "num-traits", 510 | "regex", 511 | "serde_json", 512 | "thiserror", 513 | "toml", 514 | "winnow", 515 | ] 516 | 517 | [[package]] 518 | name = "strsim" 519 | version = "0.11.1" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 522 | 523 | [[package]] 524 | name = "syn" 525 | version = "2.0.98" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" 528 | dependencies = [ 529 | "proc-macro2", 530 | "quote", 531 | "unicode-ident", 532 | ] 533 | 534 | [[package]] 535 | name = "thiserror" 536 | version = "2.0.11" 537 | source = "registry+https://github.com/rust-lang/crates.io-index" 538 | checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" 539 | dependencies = [ 540 | "thiserror-impl", 541 | ] 542 | 543 | [[package]] 544 | name = "thiserror-impl" 545 | version = "2.0.11" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" 548 | dependencies = [ 549 | "proc-macro2", 550 | "quote", 551 | "syn", 552 | ] 553 | 554 | [[package]] 555 | name = "toml" 556 | version = "0.8.20" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" 559 | dependencies = [ 560 | "serde", 561 | "serde_spanned", 562 | "toml_datetime", 563 | "toml_edit", 564 | ] 565 | 566 | [[package]] 567 | name = "toml_datetime" 568 | version = "0.6.8" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" 571 | dependencies = [ 572 | "serde", 573 | ] 574 | 575 | [[package]] 576 | name = "toml_edit" 577 | version = "0.22.24" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" 580 | dependencies = [ 581 | "indexmap", 582 | "serde", 583 | "serde_spanned", 584 | "toml_datetime", 585 | "winnow", 586 | ] 587 | 588 | [[package]] 589 | name = "unicode-ident" 590 | version = "1.0.16" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" 593 | 594 | [[package]] 595 | name = "unicode-width" 596 | version = "0.2.0" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" 599 | 600 | [[package]] 601 | name = "utf8parse" 602 | version = "0.2.2" 603 | source = "registry+https://github.com/rust-lang/crates.io-index" 604 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 605 | 606 | [[package]] 607 | name = "which" 608 | version = "7.0.2" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "2774c861e1f072b3aadc02f8ba886c26ad6321567ecc294c935434cad06f1283" 611 | dependencies = [ 612 | "either", 613 | "env_home", 614 | "rustix", 615 | "winsafe", 616 | ] 617 | 618 | [[package]] 619 | name = "windows-sys" 620 | version = "0.59.0" 621 | source = "registry+https://github.com/rust-lang/crates.io-index" 622 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 623 | dependencies = [ 624 | "windows-targets", 625 | ] 626 | 627 | [[package]] 628 | name = "windows-targets" 629 | version = "0.52.6" 630 | source = "registry+https://github.com/rust-lang/crates.io-index" 631 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 632 | dependencies = [ 633 | "windows_aarch64_gnullvm", 634 | "windows_aarch64_msvc", 635 | "windows_i686_gnu", 636 | "windows_i686_gnullvm", 637 | "windows_i686_msvc", 638 | "windows_x86_64_gnu", 639 | "windows_x86_64_gnullvm", 640 | "windows_x86_64_msvc", 641 | ] 642 | 643 | [[package]] 644 | name = "windows_aarch64_gnullvm" 645 | version = "0.52.6" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 648 | 649 | [[package]] 650 | name = "windows_aarch64_msvc" 651 | version = "0.52.6" 652 | source = "registry+https://github.com/rust-lang/crates.io-index" 653 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 654 | 655 | [[package]] 656 | name = "windows_i686_gnu" 657 | version = "0.52.6" 658 | source = "registry+https://github.com/rust-lang/crates.io-index" 659 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 660 | 661 | [[package]] 662 | name = "windows_i686_gnullvm" 663 | version = "0.52.6" 664 | source = "registry+https://github.com/rust-lang/crates.io-index" 665 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 666 | 667 | [[package]] 668 | name = "windows_i686_msvc" 669 | version = "0.52.6" 670 | source = "registry+https://github.com/rust-lang/crates.io-index" 671 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 672 | 673 | [[package]] 674 | name = "windows_x86_64_gnu" 675 | version = "0.52.6" 676 | source = "registry+https://github.com/rust-lang/crates.io-index" 677 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 678 | 679 | [[package]] 680 | name = "windows_x86_64_gnullvm" 681 | version = "0.52.6" 682 | source = "registry+https://github.com/rust-lang/crates.io-index" 683 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 684 | 685 | [[package]] 686 | name = "windows_x86_64_msvc" 687 | version = "0.52.6" 688 | source = "registry+https://github.com/rust-lang/crates.io-index" 689 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 690 | 691 | [[package]] 692 | name = "winnow" 693 | version = "0.7.2" 694 | source = "registry+https://github.com/rust-lang/crates.io-index" 695 | checksum = "59690dea168f2198d1a3b0cac23b8063efcd11012f10ae4698f284808c8ef603" 696 | dependencies = [ 697 | "memchr", 698 | ] 699 | 700 | [[package]] 701 | name = "winsafe" 702 | version = "0.0.19" 703 | source = "registry+https://github.com/rust-lang/crates.io-index" 704 | checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" 705 | 706 | [[package]] 707 | name = "x" 708 | version = "0.0.0" 709 | dependencies = [ 710 | "clap", 711 | "which", 712 | ] 713 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 tison 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | [workspace] 16 | members = ["spath", "xtask"] 17 | resolver = "2" 18 | 19 | [workspace.package] 20 | edition = "2021" 21 | homepage = "https://github.com/cratesland/spath" 22 | license = "Apache-2.0" 23 | readme = "README.md" 24 | repository = "https://github.com/cratesland/spath" 25 | rust-version = "1.80.0" 26 | version = "0.3.1" 27 | 28 | [workspace.lints.rust] 29 | unknown_lints = "deny" 30 | 31 | [workspace.lints.clippy] 32 | dbg_macro = "deny" 33 | 34 | [workspace.metadata.release] 35 | pre-release-commit-message = "chore: release v{{version}}" 36 | shared-version = true 37 | sign-tag = true 38 | tag-name = "v{{version}}" 39 | -------------------------------------------------------------------------------- /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 | # SPath: Query expressions for semi-structured data 2 | 3 | [![Crates.io][crates-badge]][crates-url] 4 | [![Documentation][docs-badge]][docs-url] 5 | [![MSRV 1.80][msrv-badge]](https://www.whatrustisit.com) 6 | [![Apache 2.0 licensed][license-badge]][license-url] 7 | [![Build Status][actions-badge]][actions-url] 8 | 9 | [crates-badge]: https://img.shields.io/crates/v/spath.svg 10 | [crates-url]: https://crates.io/crates/spath 11 | [docs-badge]: https://docs.rs/spath/badge.svg 12 | [msrv-badge]: https://img.shields.io/badge/MSRV-1.80-green?logo=rust 13 | [docs-url]: https://docs.rs/spath 14 | [license-badge]: https://img.shields.io/crates/l/spath 15 | [license-url]: LICENSE 16 | [actions-badge]: https://github.com/cratesland/spath/workflows/CI/badge.svg 17 | [actions-url]:https://github.com/cratesland/spath/actions?query=workflow%3ACI 18 | 19 | ## Overview 20 | 21 | You can use it as a drop-in replacement for JSONPath, but also for other semi-structured data formats like TOML or user-defined variants. 22 | 23 | ## Documentation 24 | 25 | * [API documentation on docs.rs](https://docs.rs/spath) 26 | 27 | ## Example 28 | 29 | Here is a quick example that shows how to use the `spath` crate to query JSONPath alike expression over JSON data: 30 | 31 | ```rust 32 | use serde_json::json; 33 | use spath::SPath; 34 | 35 | #[test] 36 | fn main() { 37 | let data = json!({ 38 | "name": "John Doe", 39 | "age": 43, 40 | "phones": [ 41 | "+44 1234567", 42 | "+44 2345678" 43 | ] 44 | }); 45 | 46 | let registry = spath::json::BuiltinFunctionRegistry::default(); 47 | let spath = SPath::parse_with_registry("$.phones[1]", registry).unwrap(); 48 | let result = spath.query(&data); 49 | let result = result.exactly_one().unwrap(); 50 | assert_eq!(result, &json!("+44 2345678")); 51 | } 52 | ``` 53 | 54 | ## Usage 55 | 56 | `spath` is [on crates.io](https://crates.io/crates/spath) and can be used by adding `spath` to your dependencies in your project's `Cargo.toml`. Or more simply, just run `cargo add spath`. 57 | 58 | ## License 59 | 60 | This project is licensed under [Apache License, Version 2.0](LICENSE). 61 | 62 | ## History 63 | 64 | From 0.3.0, this crate is reimplemented as a fork of [serde_json_path](https://crates.io/crates/serde_json_path), with modifications: 65 | 66 | * Support other semi-structured data values 67 | * Rewrite the parser with winnow + logos 68 | * Redesign the function registry 69 | * `impl Ord for PathElement` 70 | * Drop Integer wrapper (although it's a MUST in RFC 9535, I don't find the reason and highly suspect it's because JSON has only numbers (IEEE 754 float)) 71 | * Drop serde related impls. 72 | -------------------------------------------------------------------------------- /licenserc.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 tison 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | headerPath = "Apache-2.0.txt" 16 | 17 | excludes = [ 18 | 'spath/src/value/map.rs', # copied from serde_json 19 | ] 20 | includes = ['**/*.proto', '**/*.rs', '**/*.yml', '**/*.yaml', '**/*.toml'] 21 | 22 | [properties] 23 | copyrightOwner = "tison " 24 | inceptionYear = 2024 25 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 tison 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | [toolchain] 16 | channel = "stable" 17 | components = ["cargo", "rustfmt", "clippy", "rust-analyzer"] 18 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 tison 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | comment_width = 120 16 | format_code_in_doc_comments = true 17 | group_imports = "StdExternalCrate" 18 | imports_granularity = "Item" 19 | wrap_comments = true 20 | -------------------------------------------------------------------------------- /spath/Cargo.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 tison 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | [package] 16 | name = "spath" 17 | 18 | description = """ 19 | SPath is query expressions for semi-structured data. You can use it 20 | as a drop-in replacement for JSONPath, but also for other 21 | semi-structured data formats like TOML or user-defined variants. 22 | """ 23 | 24 | edition.workspace = true 25 | homepage.workspace = true 26 | license.workspace = true 27 | readme.workspace = true 28 | repository.workspace = true 29 | rust-version.workspace = true 30 | version.workspace = true 31 | 32 | [package.metadata.docs.rs] 33 | all-features = true 34 | rustdoc-args = ["--cfg", "docsrs"] 35 | 36 | [features] 37 | default = [] 38 | json = ["dep:serde_json"] 39 | regex = ["dep:regex"] 40 | toml = ["dep:toml"] 41 | 42 | [dependencies] 43 | annotate-snippets = { version = "0.11.5" } 44 | logos = { version = "0.15.0" } 45 | num-cmp = { version = "0.1.0" } 46 | num-traits = { version = "0.2.19" } 47 | thiserror = { version = "2.0.8" } 48 | winnow = { version = "0.7.2" } 49 | 50 | # optional dependencies 51 | regex = { version = "1.11.1", optional = true } 52 | serde_json = { version = "1.0.133", optional = true } 53 | toml = { version = "0.8.20", optional = true } 54 | 55 | [dev-dependencies] 56 | googletest = { version = "0.13.0" } 57 | insta = { version = "1.41.1", features = ["json"] } 58 | serde_json = { version = "1.0.133" } 59 | toml = { version = "0.8.19" } 60 | 61 | [lints] 62 | workspace = true 63 | -------------------------------------------------------------------------------- /spath/src/json.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 tison 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use serde_json::Map; 16 | use serde_json::Number; 17 | use serde_json::Value; 18 | 19 | use crate::spec::function; 20 | use crate::value::ConcreteVariantArray; 21 | use crate::value::ConcreteVariantObject; 22 | use crate::value::VariantValue; 23 | use crate::FromLiteral; 24 | use crate::Literal; 25 | 26 | pub type BuiltinFunctionRegistry = function::BuiltinFunctionRegistry; 27 | 28 | impl FromLiteral for Value { 29 | fn from_literal(literal: Literal) -> Option { 30 | match literal { 31 | Literal::Int(v) => Some(Value::Number(Number::from(v))), 32 | Literal::Float(v) => Number::from_f64(v).map(Value::Number), 33 | Literal::String(v) => Some(Value::String(v)), 34 | Literal::Bool(v) => Some(Value::Bool(v)), 35 | Literal::Null => Some(Value::Null), 36 | } 37 | } 38 | } 39 | 40 | impl VariantValue for Value { 41 | type VariantArray = Vec; 42 | type VariantObject = Map; 43 | 44 | fn is_null(&self) -> bool { 45 | self.is_null() 46 | } 47 | 48 | fn is_boolean(&self) -> bool { 49 | self.is_boolean() 50 | } 51 | 52 | fn is_string(&self) -> bool { 53 | self.is_string() 54 | } 55 | 56 | fn is_array(&self) -> bool { 57 | self.is_array() 58 | } 59 | 60 | fn is_object(&self) -> bool { 61 | self.is_object() 62 | } 63 | 64 | fn as_bool(&self) -> Option { 65 | self.as_bool() 66 | } 67 | 68 | fn as_str(&self) -> Option<&str> { 69 | self.as_str() 70 | } 71 | 72 | fn as_array(&self) -> Option<&Self::VariantArray> { 73 | self.as_array() 74 | } 75 | 76 | fn as_object(&self) -> Option<&Self::VariantObject> { 77 | self.as_object() 78 | } 79 | 80 | fn is_less_than(&self, other: &Self) -> bool { 81 | fn number_less_than(left: &Number, right: &Number) -> bool { 82 | if let (Some(l), Some(r)) = (left.as_i128(), right.as_i128()) { 83 | l < r 84 | } else if let (Some(l), Some(r)) = (left.as_f64(), right.as_f64()) { 85 | l < r 86 | } else { 87 | false 88 | } 89 | } 90 | 91 | match (self, other) { 92 | (Value::Number(n1), Value::Number(n2)) => number_less_than(n1, n2), 93 | (Value::String(s1), Value::String(s2)) => s1 < s2, 94 | _ => false, 95 | } 96 | } 97 | 98 | fn is_equal_to(&self, other: &Self) -> bool { 99 | fn number_equal_to(left: &Number, right: &Number) -> bool { 100 | if let (Some(l), Some(r)) = (left.as_i128(), right.as_i128()) { 101 | l == r 102 | } else if let (Some(l), Some(r)) = (left.as_f64(), right.as_f64()) { 103 | l == r 104 | } else { 105 | false 106 | } 107 | } 108 | 109 | match (self, other) { 110 | (Value::Number(a), Value::Number(b)) => number_equal_to(a, b), 111 | _ => self == other, 112 | } 113 | } 114 | } 115 | 116 | impl ConcreteVariantArray for Vec { 117 | type Value = Value; 118 | 119 | fn is_empty(&self) -> bool { 120 | self.is_empty() 121 | } 122 | 123 | fn len(&self) -> usize { 124 | (**self).len() 125 | } 126 | 127 | fn get(&self, index: usize) -> Option<&Self::Value> { 128 | (**self).get(index) 129 | } 130 | 131 | fn iter(&self) -> impl Iterator { 132 | (**self).iter() 133 | } 134 | } 135 | 136 | impl ConcreteVariantObject for Map { 137 | type Value = Value; 138 | 139 | fn is_empty(&self) -> bool { 140 | self.is_empty() 141 | } 142 | 143 | fn len(&self) -> usize { 144 | self.len() 145 | } 146 | 147 | fn get(&self, key: &str) -> Option<&Self::Value> { 148 | self.get(key) 149 | } 150 | 151 | fn get_key_value(&self, key: &str) -> Option<(&String, &Self::Value)> { 152 | self.get_key_value(key) 153 | } 154 | 155 | fn iter(&self) -> impl Iterator { 156 | self.iter() 157 | } 158 | 159 | fn values(&self) -> impl Iterator { 160 | self.values() 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /spath/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 tison 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! # SPath: Query expressions for semi-structured data 16 | 17 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] 18 | 19 | mod node; 20 | pub use node::*; 21 | 22 | mod path; 23 | pub use path::*; 24 | 25 | mod spath; 26 | pub use spath::*; 27 | 28 | pub mod spec; 29 | 30 | mod value; 31 | pub use value::*; 32 | 33 | mod parser; 34 | 35 | #[cfg(feature = "json")] 36 | pub mod json; 37 | #[cfg(feature = "toml")] 38 | pub mod toml; 39 | 40 | /// An error that can occur during parsing the SPath query. 41 | #[derive(Debug)] 42 | pub struct ParseError { 43 | source: String, 44 | range: std::ops::Range, 45 | message: String, 46 | } 47 | 48 | impl std::fmt::Display for ParseError { 49 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 50 | use annotate_snippets::Level; 51 | use annotate_snippets::Renderer; 52 | use annotate_snippets::Snippet; 53 | 54 | let message = Level::Error.title("failed to parse SPath query").snippet( 55 | Snippet::source(self.source.as_str()).annotation( 56 | Level::Error 57 | .span(self.range.clone()) 58 | .label(self.message.as_str()), 59 | ), 60 | ); 61 | 62 | let render = Renderer::plain(); 63 | write!(f, "{}", render.render(message))?; 64 | Ok(()) 65 | } 66 | } 67 | 68 | impl std::error::Error for ParseError {} 69 | -------------------------------------------------------------------------------- /spath/src/node.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 tison 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::iter::FusedIterator; 16 | use std::slice::Iter; 17 | 18 | use crate::path::NormalizedPath; 19 | use crate::value::VariantValue; 20 | 21 | /// A list of nodes resulting from an SPath query. 22 | /// 23 | /// Each node within the list is a borrowed reference to the node in the original 24 | /// [`VariantValue`] that was queried. 25 | #[derive(Debug, Default, Eq, PartialEq, Clone)] 26 | pub struct NodeList<'a, T: VariantValue>(Vec<&'a T>); 27 | 28 | impl<'a, T: VariantValue> NodeList<'a, T> { 29 | /// Create a new [`NodeList`] from a vector of nodes 30 | pub fn new(nodes: Vec<&'a T>) -> Self { 31 | Self(nodes) 32 | } 33 | 34 | /// Extract *at most* one node from a [`NodeList`] 35 | /// 36 | /// This is intended for queries that are expected to optionally yield a single node. 37 | pub fn at_most_one(&self) -> Result, AtMostOneError> { 38 | if self.0.len() > 1 { 39 | Err(AtMostOneError(self.0.len())) 40 | } else { 41 | Ok(self.0.first().copied()) 42 | } 43 | } 44 | 45 | /// Extract *exactly* one node from a [`NodeList`] 46 | /// 47 | /// This is intended for queries that are expected to yield exactly one node. 48 | pub fn exactly_one(&self) -> Result<&'a T, ExactlyOneError> { 49 | if self.0.len() > 1 { 50 | Err(ExactlyOneError::MoreThanOne(self.0.len())) 51 | } else { 52 | match self.0.first() { 53 | Some(node) => Ok(*node), 54 | None => Err(ExactlyOneError::Empty), 55 | } 56 | } 57 | } 58 | 59 | /// Extract all nodes yielded by the query 60 | /// 61 | /// This is intended for queries that are expected to yield zero or more nodes. 62 | pub fn all(self) -> Vec<&'a T> { 63 | self.0 64 | } 65 | 66 | /// Get the length of a [`NodeList`] 67 | pub fn len(&self) -> usize { 68 | self.0.len() 69 | } 70 | 71 | /// Check if a [`NodeList`] is empty 72 | pub fn is_empty(&self) -> bool { 73 | self.0.is_empty() 74 | } 75 | 76 | /// Get an iterator over a [`NodeList`] 77 | /// 78 | /// Note that [`NodeList`] also implements [`IntoIterator`]. 79 | pub fn iter(&self) -> Iter<'_, &T> { 80 | self.0.iter() 81 | } 82 | 83 | /// Returns the first node in the [`NodeList`], or `None` if it is empty 84 | pub fn first(&self) -> Option<&'a T> { 85 | self.0.first().copied() 86 | } 87 | 88 | /// Returns the last node in the [`NodeList`], or `None` if it is empty 89 | pub fn last(&self) -> Option<&'a T> { 90 | self.0.last().copied() 91 | } 92 | 93 | /// Returns the node at the given index in the [`NodeList`], or `None` if the given index is 94 | /// out of bounds. 95 | pub fn get(&self, index: usize) -> Option<&'a T> { 96 | self.0.get(index).copied() 97 | } 98 | } 99 | 100 | impl<'a, T: VariantValue> IntoIterator for NodeList<'a, T> { 101 | type Item = &'a T; 102 | 103 | type IntoIter = std::vec::IntoIter; 104 | 105 | fn into_iter(self) -> Self::IntoIter { 106 | self.0.into_iter() 107 | } 108 | } 109 | 110 | /// A node within a variant value, along with its normalized path location. 111 | #[derive(Debug, Clone)] 112 | pub struct LocatedNode<'a, T: VariantValue> { 113 | loc: NormalizedPath<'a>, 114 | node: &'a T, 115 | } 116 | 117 | impl PartialEq for LocatedNode<'_, T> { 118 | fn eq(&self, other: &Self) -> bool { 119 | self.loc == other.loc 120 | } 121 | } 122 | 123 | impl Eq for LocatedNode<'_, T> {} 124 | 125 | impl<'a, T: VariantValue> LocatedNode<'a, T> { 126 | /// Create a new located node. 127 | pub(crate) fn new(loc: NormalizedPath<'a>, node: &'a T) -> Self { 128 | Self { loc, node } 129 | } 130 | 131 | /// Get the location of the node as a [`NormalizedPath`]. 132 | pub fn location(&self) -> &NormalizedPath<'a> { 133 | &self.loc 134 | } 135 | 136 | /// Take the location of the node as a [`NormalizedPath`]. 137 | pub fn into_location(self) -> NormalizedPath<'a> { 138 | self.loc 139 | } 140 | 141 | /// Get the node itself. 142 | pub fn node(&self) -> &'a T { 143 | self.node 144 | } 145 | } 146 | 147 | /// A list of [`LocatedNode`] resulting from an SPath query, along with their locations. 148 | #[derive(Debug, Default, Eq, PartialEq, Clone)] 149 | pub struct LocatedNodeList<'a, T: VariantValue>(Vec>); 150 | 151 | impl<'a, T: VariantValue> LocatedNodeList<'a, T> { 152 | /// Create a new [`LocatedNodeList`] from a vector of located nodes. 153 | pub fn new(nodes: Vec>) -> Self { 154 | Self(nodes) 155 | } 156 | 157 | /// Extract *at most* one entry from a [`LocatedNodeList`] 158 | /// 159 | /// This is intended for queries that are expected to optionally yield a single node. 160 | pub fn at_most_one(mut self) -> Result>, AtMostOneError> { 161 | if self.0.len() > 1 { 162 | Err(AtMostOneError(self.0.len())) 163 | } else { 164 | Ok(self.0.pop()) 165 | } 166 | } 167 | 168 | /// Extract *exactly* one entry from a [`LocatedNodeList`] 169 | /// 170 | /// This is intended for queries that are expected to yield a single node. 171 | pub fn exactly_one(mut self) -> Result, ExactlyOneError> { 172 | if self.0.is_empty() { 173 | Err(ExactlyOneError::Empty) 174 | } else if self.0.len() > 1 { 175 | Err(ExactlyOneError::MoreThanOne(self.0.len())) 176 | } else { 177 | Ok(self.0.pop().unwrap()) 178 | } 179 | } 180 | 181 | /// Extract all located nodes yielded by the query 182 | /// 183 | /// This is intended for queries that are expected to yield zero or more nodes. 184 | pub fn all(self) -> Vec> { 185 | self.0 186 | } 187 | 188 | /// Get the length of a [`LocatedNodeList`] 189 | pub fn len(&self) -> usize { 190 | self.0.len() 191 | } 192 | 193 | /// Check if a [`LocatedNodeList`] is empty 194 | pub fn is_empty(&self) -> bool { 195 | self.0.is_empty() 196 | } 197 | 198 | /// Get an iterator over a [`LocatedNodeList`] 199 | /// 200 | /// Note that [`LocatedNodeList`] also implements [`IntoIterator`]. 201 | /// 202 | /// To iterate over just locations, see [`locations`][LocatedNodeList::locations]. To iterate 203 | /// over just nodes, see [`nodes`][LocatedNodeList::nodes]. 204 | pub fn iter(&self) -> Iter<'_, LocatedNode<'a, T>> { 205 | self.0.iter() 206 | } 207 | 208 | /// Get an iterator over the locations of nodes within a [`LocatedNodeList`] 209 | pub fn locations(&self) -> Locations<'_, T> { 210 | Locations { inner: self.iter() } 211 | } 212 | 213 | /// Get an iterator over the nodes within a [`LocatedNodeList`] 214 | pub fn nodes(&self) -> Nodes<'_, T> { 215 | Nodes { inner: self.iter() } 216 | } 217 | 218 | /// Deduplicate a [`LocatedNodeList`] and return the result 219 | /// 220 | /// See also, [`dedup_in_place`][LocatedNodeList::dedup_in_place]. 221 | pub fn dedup(mut self) -> Self { 222 | self.dedup_in_place(); 223 | self 224 | } 225 | 226 | /// Deduplicate a [`LocatedNodeList`] _in-place_ 227 | /// 228 | /// See also, [`dedup`][LocatedNodeList::dedup]. 229 | pub fn dedup_in_place(&mut self) { 230 | self.0.sort_unstable_by(|l, r| l.loc.cmp(&r.loc)); 231 | self.0.dedup(); 232 | } 233 | 234 | /// Return the first entry in the [`LocatedNodeList`], or `None` if it is empty 235 | pub fn first(&self) -> Option<&LocatedNode<'a, T>> { 236 | self.0.first() 237 | } 238 | 239 | /// Return the last entry in the [`LocatedNodeList`], or `None` if it is empty 240 | pub fn last(&self) -> Option<&LocatedNode<'a, T>> { 241 | self.0.last() 242 | } 243 | 244 | /// Returns the node at the given index in the [`LocatedNodeList`], or `None` if the 245 | /// given index is out of bounds. 246 | pub fn get(&self, index: usize) -> Option<&LocatedNode<'a, T>> { 247 | self.0.get(index) 248 | } 249 | } 250 | 251 | impl<'a, T: VariantValue> IntoIterator for LocatedNodeList<'a, T> { 252 | type Item = LocatedNode<'a, T>; 253 | 254 | type IntoIter = std::vec::IntoIter; 255 | 256 | fn into_iter(self) -> Self::IntoIter { 257 | self.0.into_iter() 258 | } 259 | } 260 | 261 | /// An iterator over the locations in a [`LocatedNodeList`] 262 | /// 263 | /// Produced by the [`LocatedNodeList::locations`] method. 264 | #[derive(Debug)] 265 | pub struct Locations<'a, T: VariantValue> { 266 | inner: Iter<'a, LocatedNode<'a, T>>, 267 | } 268 | 269 | impl<'a, T: VariantValue> Iterator for Locations<'a, T> { 270 | type Item = &'a NormalizedPath<'a>; 271 | 272 | fn next(&mut self) -> Option { 273 | self.inner.next().map(|l| l.location()) 274 | } 275 | } 276 | 277 | impl DoubleEndedIterator for Locations<'_, T> { 278 | fn next_back(&mut self) -> Option { 279 | self.inner.next_back().map(|l| l.location()) 280 | } 281 | } 282 | 283 | impl ExactSizeIterator for Locations<'_, T> { 284 | fn len(&self) -> usize { 285 | self.inner.len() 286 | } 287 | } 288 | 289 | impl FusedIterator for Locations<'_, T> {} 290 | 291 | /// An iterator over the nodes in a [`LocatedNodeList`] 292 | /// 293 | /// Produced by the [`LocatedNodeList::nodes`] method. 294 | #[derive(Debug)] 295 | pub struct Nodes<'a, T: VariantValue> { 296 | inner: Iter<'a, LocatedNode<'a, T>>, 297 | } 298 | 299 | impl<'a, T: VariantValue> Iterator for Nodes<'a, T> { 300 | type Item = &'a T; 301 | 302 | fn next(&mut self) -> Option { 303 | self.inner.next().map(|l| l.node()) 304 | } 305 | } 306 | 307 | impl DoubleEndedIterator for Nodes<'_, T> { 308 | fn next_back(&mut self) -> Option { 309 | self.inner.next_back().map(|l| l.node()) 310 | } 311 | } 312 | 313 | impl ExactSizeIterator for Nodes<'_, T> { 314 | fn len(&self) -> usize { 315 | self.inner.len() 316 | } 317 | } 318 | 319 | impl FusedIterator for Nodes<'_, T> {} 320 | 321 | /// Error produced when expecting no more than one node from a query 322 | #[derive(Debug, thiserror::Error)] 323 | #[error("nodelist expected to contain at most one entry, but instead contains {0} entries")] 324 | pub struct AtMostOneError(pub usize); 325 | 326 | /// Error produced when expecting exactly one node from a query 327 | #[derive(Debug, thiserror::Error)] 328 | pub enum ExactlyOneError { 329 | /// The query resulted in an empty [`NodeList`] 330 | #[error("nodelist expected to contain one entry, but is empty")] 331 | Empty, 332 | /// The query resulted in a [`NodeList`] containing more than one node 333 | #[error("nodelist expected to contain one entry, but instead contains {0} entries")] 334 | MoreThanOne(usize), 335 | } 336 | 337 | impl ExactlyOneError { 338 | /// Check that it is the `Empty` variant 339 | pub fn is_empty(&self) -> bool { 340 | matches!(self, Self::Empty) 341 | } 342 | 343 | /// Check that it is the `MoreThanOne` variant 344 | pub fn is_more_than_one(&self) -> bool { 345 | matches!(self, Self::MoreThanOne(_)) 346 | } 347 | 348 | /// Extract the number of nodes, if it was more than one, or `None` otherwise 349 | pub fn as_more_than_one(&self) -> Option { 350 | match self { 351 | ExactlyOneError::Empty => None, 352 | ExactlyOneError::MoreThanOne(u) => Some(*u), 353 | } 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /spath/src/parser/error.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 tison 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::fmt::Debug; 16 | 17 | use winnow::error::FromExternalError; 18 | use winnow::error::ModalError; 19 | use winnow::error::Needed; 20 | use winnow::error::ParserError; 21 | 22 | use crate::parser::input::Input; 23 | use crate::parser::range::Range; 24 | use crate::spec::function::FunctionValidationError; 25 | use crate::spec::selector::filter::NonSingularQueryError; 26 | use crate::ParseError; 27 | 28 | /// An in-flight parsing error. 29 | #[derive(Debug)] 30 | pub struct Error { 31 | range: Range, 32 | message: String, 33 | cut: bool, 34 | } 35 | 36 | impl<'a, Registry> ParserError> for Error { 37 | type Inner = Self; 38 | 39 | fn from_input(input: &Input<'a, Registry>) -> Self { 40 | Self { 41 | range: input[0].span, 42 | message: "unexpected token".to_string(), 43 | cut: false, 44 | } 45 | } 46 | 47 | fn assert(input: &Input<'a, Registry>, message: &'static str) -> Self 48 | where 49 | Input<'a, Registry>: Debug, 50 | { 51 | Self { 52 | range: input[0].span, 53 | message: message.to_string(), 54 | cut: true, 55 | } 56 | } 57 | 58 | fn incomplete(_input: &Input<'a, Registry>, _needed: Needed) -> Self { 59 | unreachable!("this parser is not partial") 60 | } 61 | 62 | fn or(self, other: Self) -> Self { 63 | if self.cut { 64 | self 65 | } else { 66 | other 67 | } 68 | } 69 | 70 | fn is_backtrack(&self) -> bool { 71 | !self.cut 72 | } 73 | 74 | fn into_inner(self) -> winnow::Result { 75 | Ok(self) 76 | } 77 | } 78 | 79 | impl<'a, Registry> FromExternalError, Error> for Error { 80 | fn from_external_error(_input: &Input<'a, Registry>, err: Error) -> Self { 81 | err 82 | } 83 | } 84 | 85 | impl<'a, Registry> FromExternalError, FunctionValidationError> for Error { 86 | fn from_external_error(input: &Input<'a, Registry>, err: FunctionValidationError) -> Self { 87 | let range = input[0].span; 88 | let message = format!("{err}"); 89 | Self { 90 | range, 91 | message, 92 | cut: true, 93 | } 94 | } 95 | } 96 | 97 | impl<'a, Registry> FromExternalError, NonSingularQueryError> for Error { 98 | fn from_external_error(input: &Input<'a, Registry>, err: NonSingularQueryError) -> Self { 99 | let range = input[0].span; 100 | let message = format!("{err}"); 101 | Self { 102 | range, 103 | message, 104 | cut: false, 105 | } 106 | } 107 | } 108 | 109 | impl ModalError for Error { 110 | fn cut(mut self) -> Self { 111 | self.cut = true; 112 | self 113 | } 114 | 115 | fn backtrack(mut self) -> Self { 116 | self.cut = false; 117 | self 118 | } 119 | } 120 | 121 | impl Error { 122 | pub fn new_cut(range: Range, message: impl Into) -> Self { 123 | Self { 124 | range, 125 | message: message.into(), 126 | cut: true, 127 | } 128 | } 129 | 130 | pub fn with_message(mut self, message: impl Into) -> Self { 131 | self.message = message.into(); 132 | self 133 | } 134 | 135 | pub fn into_parse_error(self, source: impl Into) -> ParseError { 136 | ParseError { 137 | source: source.into(), 138 | range: self.range.into(), 139 | message: self.message, 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /spath/src/parser/input.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 tison 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::fmt; 16 | use std::sync::Arc; 17 | 18 | use winnow::error::ParserError; 19 | use winnow::stream::Stream; 20 | use winnow::Parser; 21 | use winnow::Stateful; 22 | 23 | use crate::parser::error::Error; 24 | use crate::parser::range::Range; 25 | use crate::parser::token::Token; 26 | use crate::parser::token::TokenKind; 27 | use crate::spec::function::FunctionRegistry; 28 | 29 | #[derive(Clone)] 30 | pub struct InputState { 31 | registry: Arc, 32 | } 33 | 34 | impl fmt::Debug for InputState { 35 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 36 | f.debug_struct("InputState").finish_non_exhaustive() 37 | } 38 | } 39 | 40 | impl InputState { 41 | pub fn new(registry: Arc) -> Self { 42 | Self { registry } 43 | } 44 | 45 | pub fn registry(&self) -> Arc { 46 | self.registry.clone() 47 | } 48 | } 49 | 50 | pub type TokenSlice<'a> = winnow::stream::TokenSlice<'a, Token<'a>>; 51 | 52 | pub type Input<'a, Registry> = Stateful, InputState>; 53 | 54 | impl<'a, Registry> Parser, &'a Token<'a>, Error> for TokenKind 55 | where 56 | Registry: FunctionRegistry, 57 | { 58 | fn parse_next(&mut self, input: &mut Input<'a, Registry>) -> Result<&'a Token<'a>, Error> { 59 | match input.first().filter(|token| token.kind == *self) { 60 | Some(_) => { 61 | // SAFETY: `first` returns `Some` if the input is not empty. 62 | let token = input.next_token().unwrap(); 63 | Ok(token) 64 | } 65 | None => { 66 | if self.is_eoi() { 67 | let start = input.first().unwrap().span.start; 68 | let end = input.last().unwrap().span.end; 69 | Err(Error::new_cut( 70 | Range::from(start..end), 71 | "failed to parse the rest of input", 72 | )) 73 | } else { 74 | let err = Error::from_input(input); 75 | Err(err.with_message(format!("expected token {self:?}"))) 76 | } 77 | } 78 | } 79 | } 80 | } 81 | 82 | pub fn text<'a, Registry>( 83 | text: &'static str, 84 | ) -> impl Parser, &'a Token<'a>, Error> 85 | where 86 | Registry: FunctionRegistry, 87 | { 88 | move |input: &mut Input<'a, Registry>| match input.first().filter(|token| token.text() == text) 89 | { 90 | Some(_) => { 91 | // SAFETY: `first` returns `Some` if the input is not empty. 92 | let token = input.next_token().unwrap(); 93 | Ok(token) 94 | } 95 | None => { 96 | let err = Error::from_input(input); 97 | Err(err.with_message(format!("expected text {text}"))) 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /spath/src/parser/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 tison 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | mod error; 16 | mod input; 17 | mod parse; 18 | mod range; 19 | mod runner; 20 | mod token; 21 | 22 | pub use runner::run_parser; 23 | -------------------------------------------------------------------------------- /spath/src/parser/range.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 tison 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::fmt; 16 | 17 | #[derive(Clone, Copy, PartialEq, Eq)] 18 | pub struct Range { 19 | pub start: usize, 20 | pub end: usize, 21 | } 22 | 23 | impl Range { 24 | pub fn start(&self) -> usize { 25 | self.start 26 | } 27 | 28 | pub fn end(&self) -> usize { 29 | self.end 30 | } 31 | } 32 | 33 | impl fmt::Debug for Range { 34 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 35 | write!(f, "{}..{}", self.start, self.end) 36 | } 37 | } 38 | 39 | impl fmt::Display for Range { 40 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 41 | write!(f, "{}..{}", self.start, self.end) 42 | } 43 | } 44 | 45 | impl From for std::ops::Range { 46 | fn from(range: Range) -> std::ops::Range { 47 | range.start..range.end 48 | } 49 | } 50 | 51 | impl From> for Range { 52 | fn from(range: std::ops::Range) -> Range { 53 | Range { 54 | start: range.start, 55 | end: range.end, 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /spath/src/parser/runner.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 tison 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::sync::Arc; 16 | 17 | use crate::parser::error::Error; 18 | use crate::parser::input::Input; 19 | use crate::parser::input::InputState; 20 | use crate::parser::input::TokenSlice; 21 | use crate::parser::parse::parse_query_main; 22 | use crate::parser::token::Token; 23 | use crate::parser::token::Tokenizer; 24 | use crate::spec::function::FunctionRegistry; 25 | use crate::spec::query::Query; 26 | use crate::ParseError; 27 | use crate::VariantValue; 28 | 29 | pub fn run_tokenizer(source: &str) -> Result, Error> { 30 | Tokenizer::new(source).collect::>() 31 | } 32 | 33 | pub fn run_parser(source: &str, registry: Arc) -> Result 34 | where 35 | T: VariantValue, 36 | Registry: FunctionRegistry, 37 | { 38 | let tokens = run_tokenizer(source).map_err(|err| err.into_parse_error(source))?; 39 | let input = TokenSlice::new(&tokens); 40 | let state = InputState::new(registry); 41 | let mut input = Input { input, state }; 42 | parse_query_main(&mut input).map_err(|err| err.into_parse_error(source)) 43 | } 44 | -------------------------------------------------------------------------------- /spath/src/parser/token.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 tison 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::fmt; 16 | 17 | use logos::Lexer; 18 | use logos::Logos; 19 | 20 | use crate::parser::error::Error; 21 | use crate::parser::range::Range; 22 | 23 | #[derive(Clone, Copy, PartialEq, Eq)] 24 | pub struct Token<'a> { 25 | pub source: &'a str, 26 | pub kind: TokenKind, 27 | pub span: Range, 28 | } 29 | 30 | impl fmt::Debug for Token<'_> { 31 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 32 | write!(f, "{:?}({:?})", self.kind, self.span) 33 | } 34 | } 35 | 36 | impl<'a> Token<'a> { 37 | pub fn new_eoi(source: &'a str) -> Self { 38 | Token { 39 | source, 40 | kind: TokenKind::EOI, 41 | span: (source.len()..source.len()).into(), 42 | } 43 | } 44 | 45 | pub fn text(&self) -> &'a str { 46 | &self.source[std::ops::Range::from(self.span)] 47 | } 48 | } 49 | 50 | pub struct Tokenizer<'a> { 51 | source: &'a str, 52 | lexer: Lexer<'a, TokenKind>, 53 | eoi: bool, 54 | } 55 | 56 | impl<'a> Tokenizer<'a> { 57 | pub fn new(source: &'a str) -> Self { 58 | Tokenizer { 59 | source, 60 | lexer: TokenKind::lexer(source), 61 | eoi: false, 62 | } 63 | } 64 | } 65 | 66 | impl<'a> Iterator for Tokenizer<'a> { 67 | type Item = Result, Error>; 68 | 69 | fn next(&mut self) -> Option { 70 | match self.lexer.next() { 71 | Some(Err(_)) => { 72 | let span = Range::from(self.lexer.span().start..self.source.len()); 73 | let message = "failed to recognize the rest tokens"; 74 | Some(Err(Error::new_cut(span, message))) 75 | } 76 | Some(Ok(kind)) => Some(Ok(Token { 77 | source: self.source, 78 | kind, 79 | span: self.lexer.span().into(), 80 | })), 81 | None => { 82 | if !self.eoi { 83 | self.eoi = true; 84 | Some(Ok(Token::new_eoi(self.source))) 85 | } else { 86 | None 87 | } 88 | } 89 | } 90 | } 91 | } 92 | 93 | #[allow(clippy::upper_case_acronyms)] 94 | #[derive(logos::Logos, Clone, Copy, Debug, PartialEq, Eq, Hash)] 95 | pub enum TokenKind { 96 | EOI, 97 | 98 | #[regex(r"[ \t\r\n\f]+", logos::skip)] 99 | Whitespace, 100 | 101 | #[regex(r#"[_a-zA-Z\u0080-\uFFFF][_a-zA-Z0-9\u0080-\uFFFF]*"#)] 102 | Identifier, 103 | 104 | #[regex(r#"'([^'\\]|\\.)*'"#)] 105 | #[regex(r#""([^"\\]|\\.)*""#)] 106 | LiteralString, 107 | 108 | #[regex(r"(-)?[0-9]+(_|[0-9])*")] 109 | LiteralInteger, 110 | 111 | #[regex(r"(-)?[0-9]+[eE][+-]?[0-9]+")] 112 | #[regex(r"(-)?[0-9]+\.[0-9]+([eE][+-]?[0-9]+)?")] 113 | LiteralFloat, 114 | 115 | // Symbols 116 | #[token("=")] 117 | #[token("==")] 118 | Eq, 119 | #[token("<>")] 120 | #[token("!=")] 121 | NotEq, 122 | #[token("!")] 123 | Not, 124 | #[token("<")] 125 | Lt, 126 | #[token(">")] 127 | Gt, 128 | #[token("<=")] 129 | Lte, 130 | #[token(">=")] 131 | Gte, 132 | #[token("&&")] 133 | And, 134 | #[token("||")] 135 | Or, 136 | #[token("$")] 137 | Dollar, 138 | #[token("@")] 139 | At, 140 | #[token(".")] 141 | Dot, 142 | #[token("..")] 143 | DoubleDot, 144 | #[token("*")] 145 | Asterisk, 146 | #[token(":")] 147 | Colon, 148 | #[token(",")] 149 | Comma, 150 | #[token("?")] 151 | QuestionMark, 152 | #[token("(")] 153 | LParen, 154 | #[token(")")] 155 | RParen, 156 | #[token("[")] 157 | LBracket, 158 | #[token("]")] 159 | RBracket, 160 | 161 | // §2.3.5.1. Syntax 162 | // true, false, and null are lowercase only (case-sensitive). 163 | #[token("false")] 164 | FALSE, 165 | #[token("null")] 166 | NULL, 167 | #[token("true")] 168 | TRUE, 169 | } 170 | 171 | impl TokenKind { 172 | pub fn is_eoi(&self) -> bool { 173 | matches!(self, TokenKind::EOI) 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /spath/src/path.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 tison 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Types for representing [Normalized Paths] from RFC 9535. 16 | //! 17 | //! [Normalized Paths]: https://datatracker.ietf.org/doc/html/rfc9535#name-normalized-paths 18 | 19 | use std::cmp::Ordering; 20 | use std::fmt; 21 | use std::slice::Iter; 22 | use std::slice::SliceIndex; 23 | 24 | #[derive(Debug, Default, Eq, PartialEq, Clone, PartialOrd, Ord)] 25 | pub struct NormalizedPath<'a>(Vec>); 26 | 27 | impl<'a> NormalizedPath<'a> { 28 | pub(crate) fn push>>(&mut self, elem: T) { 29 | self.0.push(elem.into()) 30 | } 31 | 32 | pub(crate) fn clone_and_push>>(&self, elem: T) -> Self { 33 | let mut new_path = self.clone(); 34 | new_path.push(elem.into()); 35 | new_path 36 | } 37 | 38 | /// Check if the [`NormalizedPath`] is empty 39 | /// 40 | /// An empty normalized path represents the location of the root node of the object, 41 | /// i.e., `$`. 42 | pub fn is_empty(&self) -> bool { 43 | self.0.is_empty() 44 | } 45 | 46 | /// Get the length of the [`NormalizedPath`] 47 | pub fn len(&self) -> usize { 48 | self.0.len() 49 | } 50 | 51 | /// Get an iterator over the [`PathElement`]s of the [`NormalizedPath`] 52 | /// 53 | /// Note that [`NormalizedPath`] also implements [`IntoIterator`] 54 | pub fn iter(&self) -> Iter<'_, PathElement<'a>> { 55 | self.0.iter() 56 | } 57 | 58 | /// Get the [`PathElement`] at `index`, or `None` if the index is out of bounds 59 | pub fn get(&self, index: I) -> Option<&I::Output> 60 | where 61 | I: SliceIndex<[PathElement<'a>]>, 62 | { 63 | self.0.get(index) 64 | } 65 | 66 | /// Get the first [`PathElement`], or `None` if the path is empty 67 | pub fn first(&self) -> Option<&PathElement<'a>> { 68 | self.0.first() 69 | } 70 | 71 | /// Get the last [`PathElement`], or `None` if the path is empty 72 | pub fn last(&self) -> Option<&PathElement<'a>> { 73 | self.0.last() 74 | } 75 | } 76 | 77 | impl<'a> IntoIterator for NormalizedPath<'a> { 78 | type Item = PathElement<'a>; 79 | 80 | type IntoIter = std::vec::IntoIter; 81 | 82 | fn into_iter(self) -> Self::IntoIter { 83 | self.0.into_iter() 84 | } 85 | } 86 | 87 | impl fmt::Display for NormalizedPath<'_> { 88 | /// Format the [`NormalizedPath`] as an SPath string using the canonical bracket notation 89 | /// as per [RFC 9535][norm-paths] 90 | /// 91 | /// [norm-paths]: https://datatracker.ietf.org/doc/html/rfc9535#name-normalized-paths 92 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 93 | write!(f, "$")?; 94 | for elem in &self.0 { 95 | match elem { 96 | PathElement::Name(name) => write!(f, "['{name}']")?, 97 | PathElement::Index(index) => write!(f, "[{index}]")?, 98 | } 99 | } 100 | Ok(()) 101 | } 102 | } 103 | 104 | /// An element within a [`NormalizedPath`] 105 | #[derive(Debug, Eq, PartialEq, Clone)] 106 | pub enum PathElement<'a> { 107 | /// A key within an object 108 | Name(&'a str), 109 | /// An index of an array 110 | Index(usize), 111 | } 112 | 113 | impl PathElement<'_> { 114 | /// Get the underlying name if the [`PathElement`] is `Name`, or `None` otherwise 115 | pub fn as_name(&self) -> Option<&str> { 116 | match self { 117 | PathElement::Name(n) => Some(n), 118 | PathElement::Index(_) => None, 119 | } 120 | } 121 | 122 | /// Get the underlying index if the [`PathElement`] is `Index`, or `None` otherwise 123 | pub fn as_index(&self) -> Option { 124 | match self { 125 | PathElement::Name(_) => None, 126 | PathElement::Index(i) => Some(*i), 127 | } 128 | } 129 | 130 | /// Test if the [`PathElement`] is `Name` 131 | pub fn is_name(&self) -> bool { 132 | self.as_name().is_some() 133 | } 134 | 135 | /// Test if the [`PathElement`] is `Index` 136 | pub fn is_index(&self) -> bool { 137 | self.as_index().is_some() 138 | } 139 | } 140 | 141 | impl<'a> From<&'a String> for PathElement<'a> { 142 | fn from(s: &'a String) -> Self { 143 | Self::Name(s.as_str()) 144 | } 145 | } 146 | 147 | impl From for PathElement<'_> { 148 | fn from(index: usize) -> Self { 149 | Self::Index(index) 150 | } 151 | } 152 | 153 | impl PartialOrd for PathElement<'_> { 154 | fn partial_cmp(&self, other: &Self) -> Option { 155 | Some(self.cmp(other)) 156 | } 157 | } 158 | 159 | impl Ord for PathElement<'_> { 160 | fn cmp(&self, other: &Self) -> Ordering { 161 | match (self, other) { 162 | (PathElement::Name(a), PathElement::Name(b)) => a.cmp(b), 163 | (PathElement::Index(a), PathElement::Index(b)) => a.cmp(b), 164 | (PathElement::Name(_), PathElement::Index(_)) => Ordering::Greater, 165 | (PathElement::Index(_), PathElement::Name(_)) => Ordering::Less, 166 | } 167 | } 168 | } 169 | 170 | impl PartialEq for PathElement<'_> { 171 | fn eq(&self, other: &str) -> bool { 172 | match self { 173 | PathElement::Name(s) => s.eq(&other), 174 | PathElement::Index(_) => false, 175 | } 176 | } 177 | } 178 | 179 | impl PartialEq<&str> for PathElement<'_> { 180 | fn eq(&self, other: &&str) -> bool { 181 | match self { 182 | PathElement::Name(s) => s.eq(other), 183 | PathElement::Index(_) => false, 184 | } 185 | } 186 | } 187 | 188 | impl PartialEq for PathElement<'_> { 189 | fn eq(&self, other: &usize) -> bool { 190 | match self { 191 | PathElement::Name(_) => false, 192 | PathElement::Index(i) => i.eq(other), 193 | } 194 | } 195 | } 196 | 197 | impl fmt::Display for PathElement<'_> { 198 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 199 | match self { 200 | PathElement::Name(n) => { 201 | // https://datatracker.ietf.org/doc/html/rfc9535#name-normalized-paths 202 | for c in n.chars() { 203 | match c { 204 | '\u{0008}' => write!(f, r#"\b"#)?, // b BS backspace 205 | '\u{000C}' => write!(f, r#"\f"#)?, // f FF form feed 206 | '\u{000A}' => write!(f, r#"\n"#)?, // n LF line feed 207 | '\u{000D}' => write!(f, r#"\r"#)?, // r CR carriage return 208 | '\u{0009}' => write!(f, r#"\t"#)?, // t HT horizontal tab 209 | '\u{0027}' => write!(f, r#"\'"#)?, // ' apostrophe 210 | '\u{005C}' => write!(f, r#"\"#)?, // \ backslash (reverse solidus) 211 | ('\x00'..='\x07') | '\x0b' | '\x0e' | '\x0f' => { 212 | // "00"-"07", "0b", "0e"-"0f" 213 | write!(f, "\\u000{:x}", c as i32)? 214 | } 215 | _ => write!(f, "{c}")?, 216 | } 217 | } 218 | Ok(()) 219 | } 220 | PathElement::Index(i) => write!(f, "{i}"), 221 | } 222 | } 223 | } 224 | 225 | #[cfg(test)] 226 | mod tests { 227 | use insta::assert_snapshot; 228 | 229 | use super::PathElement; 230 | 231 | #[test] 232 | fn test_normalized_element() { 233 | // simple name 234 | assert_snapshot!(PathElement::Name("foo"), @"foo"); 235 | // index 236 | assert_snapshot!(PathElement::Index(1), @"1"); 237 | // escape_apostrophes 238 | assert_snapshot!(PathElement::Name("'hi'"), @r#"\'hi\'"#); 239 | // escapes 240 | assert_snapshot!(PathElement::Name(r#"'\b\f\n\r\t\\'"#), @r#"\'\b\f\n\r\t\\\'"#); 241 | // escape_vertical_unicode 242 | assert_snapshot!(PathElement::Name("\u{000B}"), @r#"\u000b"#); 243 | // escape_unicode_null 244 | assert_snapshot!(PathElement::Name("\u{0000}"), @r#"\u0000"#); 245 | // escape_unicode_runes 246 | assert_snapshot!(PathElement::Name( 247 | "\u{0001}\u{0002}\u{0003}\u{0004}\u{0005}\u{0006}\u{0007}\u{000e}\u{000F}" 248 | ), @r#"\u0001\u0002\u0003\u0004\u0005\u0006\u0007\u000e\u000f"#); 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /spath/src/spath.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 tison 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::fmt; 16 | use std::sync::Arc; 17 | 18 | use crate::parser::run_parser; 19 | use crate::spec::function::FunctionRegistry; 20 | use crate::spec::query::Query; 21 | use crate::spec::query::Queryable; 22 | use crate::LocatedNodeList; 23 | use crate::NodeList; 24 | use crate::ParseError; 25 | use crate::VariantValue; 26 | 27 | #[derive(Debug, Clone)] 28 | pub struct SPath> { 29 | query: Query, 30 | registry: Arc, 31 | } 32 | 33 | impl> SPath { 34 | pub fn parse_with_registry(query: &str, registry: Registry) -> Result { 35 | let registry = Arc::new(registry); 36 | let query = run_parser(query, registry.clone())?; 37 | Ok(Self { query, registry }) 38 | } 39 | 40 | pub fn query<'b>(&self, value: &'b T) -> NodeList<'b, T> { 41 | let nodes = self.query.query(value, value, &self.registry); 42 | NodeList::new(nodes) 43 | } 44 | 45 | pub fn query_located<'b>(&self, value: &'b T) -> LocatedNodeList<'b, T> { 46 | let nodes = self 47 | .query 48 | .query_located(value, value, &self.registry, Default::default()); 49 | LocatedNodeList::new(nodes) 50 | } 51 | } 52 | 53 | impl> fmt::Display for SPath { 54 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 55 | write!(f, "{}", self.query) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /spath/src/spec/function/builtin.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 tison 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use num_traits::ToPrimitive; 16 | 17 | use crate::spec::function::Function; 18 | use crate::spec::function::SPathType; 19 | use crate::spec::function::SPathValue; 20 | use crate::spec::function::ValueType; 21 | use crate::ConcreteVariantArray; 22 | use crate::ConcreteVariantObject; 23 | use crate::Literal; 24 | use crate::VariantValue; 25 | 26 | pub fn length() -> Function { 27 | Function::new( 28 | "length", 29 | vec![SPathType::Value], 30 | SPathType::Value, 31 | Box::new(move |mut args| { 32 | assert_eq!(args.len(), 1); 33 | 34 | fn value_len(t: &T) -> Option { 35 | if let Some(s) = t.as_str() { 36 | Some(s.chars().count()) 37 | } else if let Some(a) = t.as_array() { 38 | Some(a.len()) 39 | } else { 40 | t.as_object().map(|o| o.len()) 41 | } 42 | } 43 | 44 | let value = args.pop().unwrap().into_value().unwrap(); 45 | 46 | let len = match value { 47 | ValueType::Value(v) => value_len(&v), 48 | ValueType::Node(v) => value_len(v), 49 | ValueType::Nothing => None, 50 | } 51 | .and_then(|l| l.to_i64()) 52 | .and_then(|len| T::from_literal(Literal::Int(len))); 53 | 54 | match len { 55 | Some(v) => SPathValue::Value(v), 56 | None => SPathValue::Nothing, 57 | } 58 | }), 59 | ) 60 | } 61 | 62 | pub fn count() -> Function { 63 | Function::new( 64 | "count", 65 | vec![SPathType::Nodes], 66 | SPathType::Value, 67 | Box::new(move |mut args| { 68 | assert_eq!(args.len(), 1); 69 | 70 | let nodes = args.pop().unwrap().into_nodes().unwrap(); 71 | 72 | let len = nodes 73 | .len() 74 | .to_i64() 75 | .and_then(|len| T::from_literal(Literal::Int(len))); 76 | 77 | match len { 78 | Some(v) => SPathValue::Value(v), 79 | None => SPathValue::Nothing, 80 | } 81 | }), 82 | ) 83 | } 84 | 85 | pub fn value() -> Function { 86 | Function::new( 87 | "value", 88 | vec![SPathType::Nodes], 89 | SPathType::Value, 90 | Box::new(move |mut args| { 91 | assert_eq!(args.len(), 1); 92 | 93 | let value = args.pop().unwrap().into_nodes().unwrap(); 94 | if value.len() > 1 { 95 | SPathValue::Nothing 96 | } else { 97 | match value.first() { 98 | Some(v) => SPathValue::Node(v), 99 | None => SPathValue::Nothing, 100 | } 101 | } 102 | }), 103 | ) 104 | } 105 | 106 | // 'match' is a keyword in Rust, so we use 'matches' instead. 107 | #[cfg(feature = "regex")] 108 | pub fn matches() -> Function { 109 | Function::new( 110 | "match", 111 | vec![SPathType::Value, SPathType::Value], 112 | SPathType::Logical, 113 | Box::new(move |mut args| { 114 | assert_eq!(args.len(), 2); 115 | 116 | let matcher = args.pop().unwrap().into_value().unwrap(); 117 | let expr = args.pop().unwrap().into_value().unwrap(); 118 | 119 | let matches = match ( 120 | matcher.as_value().and_then(|v| v.as_str()), 121 | expr.as_value().and_then(|v| v.as_str()), 122 | ) { 123 | (Some(r), Some(s)) => regex::Regex::new(format!("(?R)^({r})$").as_str()) 124 | .map(|r| r.is_match(s)) 125 | .unwrap_or_default(), 126 | _ => false, 127 | }; 128 | 129 | SPathValue::Logical(matches.into()) 130 | }), 131 | ) 132 | } 133 | 134 | #[cfg(feature = "regex")] 135 | pub fn search() -> Function { 136 | Function::new( 137 | "search", 138 | vec![SPathType::Value, SPathType::Value], 139 | SPathType::Logical, 140 | Box::new(move |mut args| { 141 | assert_eq!(args.len(), 2); 142 | 143 | let matcher = args.pop().unwrap().into_value().unwrap(); 144 | let expr = args.pop().unwrap().into_value().unwrap(); 145 | 146 | let matches = match ( 147 | matcher.as_value().and_then(|v| v.as_str()), 148 | expr.as_value().and_then(|v| v.as_str()), 149 | ) { 150 | (Some(r), Some(s)) => regex::Regex::new(format!("(?R)({r})").as_str()) 151 | .map(|r| r.is_match(s)) 152 | .unwrap_or_default(), 153 | _ => false, 154 | }; 155 | 156 | SPathValue::Logical(matches.into()) 157 | }), 158 | ) 159 | } 160 | -------------------------------------------------------------------------------- /spath/src/spec/function/expr.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 tison 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::fmt; 16 | 17 | use crate::spec::function::types::FunctionArgType; 18 | use crate::spec::function::types::SPathType; 19 | use crate::spec::function::value::LogicalType; 20 | use crate::spec::function::value::SPathValue; 21 | use crate::spec::function::FunctionRegistry; 22 | use crate::spec::query::Query; 23 | use crate::spec::query::Queryable; 24 | use crate::spec::selector::filter::LogicalOrExpr; 25 | use crate::spec::selector::filter::SingularQuery; 26 | use crate::spec::selector::filter::TestFilter; 27 | use crate::Literal; 28 | use crate::NodeList; 29 | use crate::VariantValue; 30 | 31 | #[doc(hidden)] 32 | #[derive(Debug, Clone)] 33 | pub struct FunctionExpr { 34 | pub name: String, 35 | pub args: Vec, 36 | pub return_type: SPathType, 37 | } 38 | 39 | impl FunctionExpr { 40 | pub fn evaluate<'a, 'b: 'a, T: VariantValue, Registry: FunctionRegistry>( 41 | &'a self, 42 | current: &'b T, 43 | root: &'b T, 44 | registry: &Registry, 45 | ) -> SPathValue<'a, T> { 46 | let args: Vec> = self 47 | .args 48 | .iter() 49 | .map(|a| a.evaluate(current, root, registry)) 50 | .collect(); 51 | // SAFETY: upon evaluation, the function is guaranteed to be validated 52 | let f = registry.get(self.name.as_str()).unwrap(); 53 | f.evaluate(args) 54 | } 55 | 56 | pub fn validate( 57 | name: String, 58 | args: Vec, 59 | registry: &Registry, 60 | ) -> Result<(), FunctionValidationError> { 61 | let f = registry 62 | .get(name.as_str()) 63 | .ok_or(FunctionValidationError::Undefined { name })?; 64 | f.validate(args.as_slice(), registry) 65 | } 66 | } 67 | 68 | impl fmt::Display for FunctionExpr { 69 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 70 | write!(f, "{name}(", name = self.name)?; 71 | for (i, arg) in self.args.iter().enumerate() { 72 | write!( 73 | f, 74 | "{arg}{comma}", 75 | comma = if i == self.args.len() - 1 { "" } else { "," } 76 | )?; 77 | } 78 | write!(f, ")") 79 | } 80 | } 81 | 82 | impl TestFilter for FunctionExpr { 83 | fn test_filter<'b, T: VariantValue, Registry: FunctionRegistry>( 84 | &self, 85 | current: &'b T, 86 | root: &'b T, 87 | registry: &Registry, 88 | ) -> bool { 89 | match self.evaluate(current, root, registry) { 90 | SPathValue::Logical(l) => l.into(), 91 | SPathValue::Nodes(nodes) => !nodes.is_empty(), 92 | SPathValue::Value(_) => unreachable!("testable function never returns a value"), 93 | SPathValue::Node(_) => unreachable!("testable function never returns a node"), 94 | SPathValue::Nothing => unreachable!("testable function never returns nothing"), 95 | } 96 | } 97 | } 98 | 99 | #[doc(hidden)] 100 | #[derive(Debug, Clone)] 101 | pub enum FunctionExprArg { 102 | Literal(Literal), 103 | SingularQuery(SingularQuery), 104 | FilterQuery(Query), 105 | LogicalExpr(LogicalOrExpr), 106 | FunctionExpr(FunctionExpr), 107 | } 108 | 109 | impl fmt::Display for FunctionExprArg { 110 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 111 | match self { 112 | FunctionExprArg::Literal(lit) => write!(f, "{lit}"), 113 | FunctionExprArg::FilterQuery(query) => write!(f, "{query}"), 114 | FunctionExprArg::SingularQuery(sq) => write!(f, "{sq}"), 115 | FunctionExprArg::LogicalExpr(log) => write!(f, "{log}"), 116 | FunctionExprArg::FunctionExpr(func) => write!(f, "{func}"), 117 | } 118 | } 119 | } 120 | 121 | impl FunctionExprArg { 122 | fn evaluate<'a, 'b: 'a, T: VariantValue, Registry: FunctionRegistry>( 123 | &'a self, 124 | current: &'b T, 125 | root: &'b T, 126 | registry: &Registry, 127 | ) -> SPathValue<'a, T> { 128 | match self { 129 | FunctionExprArg::Literal(lit) => match T::from_literal(lit.clone()) { 130 | None => SPathValue::Nothing, 131 | Some(v) => SPathValue::Value(v), 132 | }, 133 | FunctionExprArg::SingularQuery(q) => match q.eval_query(current, root) { 134 | Some(n) => SPathValue::Node(n), 135 | None => SPathValue::Nothing, 136 | }, 137 | FunctionExprArg::FilterQuery(q) => { 138 | let nodes = q.query(current, root, registry); 139 | SPathValue::Nodes(NodeList::new(nodes)) 140 | } 141 | FunctionExprArg::LogicalExpr(l) => match l.test_filter(current, root, registry) { 142 | true => SPathValue::Logical(LogicalType::True), 143 | false => SPathValue::Logical(LogicalType::False), 144 | }, 145 | FunctionExprArg::FunctionExpr(f) => f.evaluate(current, root, registry), 146 | } 147 | } 148 | 149 | pub fn as_type_kind( 150 | &self, 151 | registry: &Registry, 152 | ) -> Result { 153 | match self { 154 | FunctionExprArg::Literal(_) => Ok(FunctionArgType::Literal), 155 | FunctionExprArg::SingularQuery(_) => Ok(FunctionArgType::SingularQuery), 156 | FunctionExprArg::FilterQuery(query) => { 157 | if query.is_singular() { 158 | Ok(FunctionArgType::SingularQuery) 159 | } else { 160 | Ok(FunctionArgType::NodeList) 161 | } 162 | } 163 | FunctionExprArg::LogicalExpr(_) => Ok(FunctionArgType::Logical), 164 | FunctionExprArg::FunctionExpr(func) => registry 165 | .get(func.name.as_str()) 166 | .map(|f| f.result_type().as_function_arg_type()) 167 | .ok_or_else(|| FunctionValidationError::Undefined { 168 | name: func.name.to_string(), 169 | }), 170 | } 171 | } 172 | } 173 | 174 | /// An error occurred while validating a function 175 | #[derive(Debug, thiserror::Error, PartialEq)] 176 | pub enum FunctionValidationError { 177 | /// Function not defined in inventory 178 | #[error("function '{name}' is not defined")] 179 | Undefined { 180 | /// The name of the function 181 | name: String, 182 | }, 183 | /// Mismatch in number of function arguments 184 | #[error("function '{name}' expects {expected} args, but received {received}")] 185 | NumberOfArgsMismatch { 186 | /// Function name. 187 | name: String, 188 | /// Expected number of arguments. 189 | expected: usize, 190 | /// Received number of arguments. 191 | received: usize, 192 | }, 193 | /// The type of received argument does not match the function definition 194 | #[error("function '{name}' argument [{position}] expects a type that converts to <{expected}>, but received <{received}>")] 195 | MismatchTypeKind { 196 | /// Function name. 197 | name: String, 198 | /// Expected type. 199 | expected: SPathType, 200 | /// Received type. 201 | received: FunctionArgType, 202 | /// Argument position. 203 | position: usize, 204 | }, 205 | #[error("function '{name}' returns <{received:?}>, but expected any of <{expected:?}>")] 206 | IncorrectFunctionReturnType { 207 | /// Function name. 208 | name: String, 209 | /// Expected return type. 210 | expected: Vec, 211 | /// Received return type. 212 | received: SPathType, 213 | }, 214 | } 215 | -------------------------------------------------------------------------------- /spath/src/spec/function/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 tison 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::fmt; 16 | use std::marker::PhantomData; 17 | use std::sync::Arc; 18 | 19 | use crate::spec::function::builtin::*; 20 | use crate::VariantValue; 21 | 22 | pub mod builtin; 23 | 24 | mod expr; 25 | pub use expr::*; 26 | 27 | mod types; 28 | pub use types::*; 29 | 30 | mod value; 31 | pub use value::*; 32 | 33 | pub type Evaluator = Box>) -> SPathValue>; 34 | 35 | pub struct Function { 36 | name: &'static str, 37 | argument_types: Vec, 38 | result_type: SPathType, 39 | evaluator: Evaluator, 40 | } 41 | 42 | impl fmt::Debug for Function { 43 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 44 | f.debug_struct("Function") 45 | .field("name", &self.name) 46 | .field("argument_types", &self.argument_types) 47 | .field("result_type", &self.result_type) 48 | .finish_non_exhaustive() 49 | } 50 | } 51 | 52 | impl Function { 53 | /// Create a new function instance. 54 | pub fn new( 55 | name: &'static str, 56 | argument_types: Vec, 57 | result_type: SPathType, 58 | evaluator: Evaluator, 59 | ) -> Self { 60 | Self { 61 | name, 62 | argument_types, 63 | result_type, 64 | evaluator, 65 | } 66 | } 67 | 68 | /// The name of the function. 69 | pub fn name(&self) -> &str { 70 | self.name 71 | } 72 | 73 | /// The declared types of function's arguments. 74 | pub fn argument_types(&self) -> &[SPathType] { 75 | self.argument_types.as_slice() 76 | } 77 | 78 | /// The return type of the function. 79 | pub fn result_type(&self) -> SPathType { 80 | self.result_type 81 | } 82 | 83 | /// Evaluate the function with args. 84 | pub fn evaluate<'a>(&self, args: Vec>) -> SPathValue<'a, T> { 85 | (self.evaluator)(args) 86 | } 87 | 88 | /// Validate the type of function arguments. 89 | pub fn validate>( 90 | &self, 91 | args: &[FunctionExprArg], 92 | registry: &Registry, 93 | ) -> Result<(), FunctionValidationError> { 94 | let argument_types = self.argument_types(); 95 | 96 | if args.len() != argument_types.len() { 97 | return Err(FunctionValidationError::NumberOfArgsMismatch { 98 | name: self.name().to_string(), 99 | expected: 1, 100 | received: args.len(), 101 | }); 102 | } 103 | 104 | for (i, arg) in args.iter().enumerate() { 105 | let ty = argument_types[i]; 106 | let kind = arg.as_type_kind(registry)?; 107 | if !kind.converts_to(ty) { 108 | return Err(FunctionValidationError::MismatchTypeKind { 109 | name: self.name().to_string(), 110 | expected: ty, 111 | received: kind, 112 | position: i, 113 | }); 114 | } 115 | } 116 | 117 | Ok(()) 118 | } 119 | } 120 | 121 | #[doc(hidden)] 122 | pub trait FunctionRegistry { 123 | type Value: VariantValue; 124 | fn get(&self, name: &str) -> Option>; 125 | } 126 | 127 | impl FunctionRegistry for Arc 128 | where 129 | Registry: FunctionRegistry, 130 | { 131 | type Value = Registry::Value; 132 | 133 | fn get(&self, name: &str) -> Option> { 134 | (**self).get(name) 135 | } 136 | } 137 | 138 | #[derive(Debug, Clone, Copy)] 139 | pub struct BuiltinFunctionRegistry { 140 | phantom: PhantomData, 141 | } 142 | 143 | impl Default for BuiltinFunctionRegistry { 144 | fn default() -> Self { 145 | Self { 146 | phantom: PhantomData, 147 | } 148 | } 149 | } 150 | 151 | impl FunctionRegistry for BuiltinFunctionRegistry { 152 | type Value = T; 153 | 154 | fn get(&self, name: &str) -> Option> { 155 | match name.to_lowercase().as_str() { 156 | "count" => Some(count()), 157 | "length" => Some(length()), 158 | "value" => Some(value()), 159 | #[cfg(feature = "regex")] 160 | "match" => Some(matches()), 161 | #[cfg(feature = "regex")] 162 | "search" => Some(search()), 163 | _ => None, 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /spath/src/spec/function/types.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 tison 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::fmt; 16 | 17 | /// The type system of SPath values. 18 | #[derive(Debug, Clone, Copy, PartialEq)] 19 | pub enum SPathType { 20 | /// A list of nodes. 21 | Nodes, 22 | /// A singular variant value. 23 | Value, 24 | /// A logical value. 25 | Logical, 26 | } 27 | 28 | impl SPathType { 29 | /// Convert the SPath type to a function argument type. 30 | pub fn as_function_arg_type(&self) -> FunctionArgType { 31 | match self { 32 | SPathType::Nodes => FunctionArgType::NodeList, 33 | SPathType::Value => FunctionArgType::Value, 34 | SPathType::Logical => FunctionArgType::Logical, 35 | } 36 | } 37 | } 38 | 39 | impl fmt::Display for SPathType { 40 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 41 | match self { 42 | SPathType::Nodes => write!(f, "nodes type"), 43 | SPathType::Logical => write!(f, "logical type"), 44 | SPathType::Value => write!(f, "value type"), 45 | } 46 | } 47 | } 48 | 49 | /// Function argument types. 50 | /// 51 | /// This is used to describe the type of function argument to determine if it will be valid as a 52 | /// parameter to the function it is being passed to. 53 | /// 54 | /// The reason for having this type in addition to [`SPathType`] is that we need to have an 55 | /// intermediate representation of arguments that are singular queries. This is because singular 56 | /// queries can be used as an argument to both [`ValueType`] and [`NodesType`] parameters. 57 | /// Therefore, we require a `Node` variant here to indicate that an argument may be converted into 58 | /// either type of parameter. 59 | #[doc(hidden)] 60 | #[derive(Debug, PartialEq, Eq, Clone, Copy)] 61 | pub enum FunctionArgType { 62 | /// Denotes a literal owned variant value 63 | Literal, 64 | /// Denotes a borrowed variant value from a singular query 65 | SingularQuery, 66 | /// Denotes a literal or borrowed variant value, used to represent functions that return 67 | /// [`ValueType`] 68 | Value, 69 | /// Denotes a node list, either from a filter query argument, or a function that returns 70 | /// [`NodesType`] 71 | NodeList, 72 | /// Denotes a logical, either from a logical expression, or from a function that returns 73 | /// [`LogicalType`] 74 | Logical, 75 | } 76 | 77 | impl fmt::Display for FunctionArgType { 78 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 79 | match self { 80 | FunctionArgType::Literal => write!(f, "literal"), 81 | FunctionArgType::SingularQuery => write!(f, "singular query"), 82 | FunctionArgType::Value => write!(f, "value type"), 83 | FunctionArgType::NodeList => write!(f, "node list type"), 84 | FunctionArgType::Logical => write!(f, "logical type"), 85 | } 86 | } 87 | } 88 | 89 | impl FunctionArgType { 90 | pub fn converts_to(&self, spath_type: SPathType) -> bool { 91 | matches!( 92 | (self, spath_type), 93 | ( 94 | FunctionArgType::Literal | FunctionArgType::Value, 95 | SPathType::Value 96 | ) | ( 97 | FunctionArgType::SingularQuery, 98 | SPathType::Value | SPathType::Nodes | SPathType::Logical 99 | ) | ( 100 | FunctionArgType::NodeList, 101 | SPathType::Nodes | SPathType::Logical 102 | ) | (FunctionArgType::Logical, SPathType::Logical), 103 | ) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /spath/src/spec/function/value.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 tison 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::ops::Deref; 16 | use std::ops::DerefMut; 17 | 18 | use crate::NodeList; 19 | use crate::VariantValue; 20 | 21 | /// SPath generic value. 22 | #[derive(Debug)] 23 | pub enum SPathValue<'a, T: VariantValue> { 24 | Nodes(NodeList<'a, T>), 25 | Logical(LogicalType), 26 | Node(&'a T), 27 | Value(T), 28 | Nothing, 29 | } 30 | 31 | impl<'a, T: VariantValue> SPathValue<'a, T> { 32 | /// Convert self to a node list if possible. 33 | pub fn into_nodes(self) -> Option> { 34 | match self { 35 | SPathValue::Nodes(nodes) => Some(nodes.into()), 36 | _ => None, 37 | } 38 | } 39 | 40 | /// Convert self to a logical value if possible. 41 | /// 42 | /// §2.4.2. Type Conversion 43 | /// 44 | /// If the nodelist contains one or more nodes, the conversion result is LogicalTrue. 45 | /// 46 | /// If the nodelist is empty, the conversion result is LogicalFalse. 47 | pub fn into_logical(self) -> Option { 48 | match self { 49 | SPathValue::Logical(logical) => Some(logical), 50 | SPathValue::Nodes(nodes) => Some(LogicalType::from(!nodes.is_empty())), 51 | _ => None, 52 | } 53 | } 54 | 55 | /// Convert self to a singular optional value if possible. 56 | pub fn into_value(self) -> Option> { 57 | match self { 58 | SPathValue::Value(value) => Some(ValueType::Value(value)), 59 | SPathValue::Node(node) => Some(ValueType::Node(node)), 60 | SPathValue::Nothing => Some(ValueType::Nothing), 61 | _ => None, 62 | } 63 | } 64 | } 65 | 66 | /// SPath value representing a node list. 67 | /// 68 | /// This is a thin wrapper around a [`NodeList`], and generally represents the result of an SPath 69 | /// query. It may also be produced by a function. 70 | #[derive(Debug, Default, PartialEq, Clone)] 71 | pub struct NodesType<'a, T: VariantValue>(NodeList<'a, T>); 72 | 73 | impl<'a, T: VariantValue> NodesType<'a, T> { 74 | /// Extract all inner nodes as a vector 75 | pub fn all(self) -> Vec<&'a T> { 76 | self.0.all() 77 | } 78 | } 79 | 80 | impl<'a, T: VariantValue> IntoIterator for NodesType<'a, T> { 81 | type Item = &'a T; 82 | type IntoIter = std::vec::IntoIter; 83 | 84 | fn into_iter(self) -> Self::IntoIter { 85 | self.0.into_iter() 86 | } 87 | } 88 | 89 | impl<'a, T: VariantValue> Deref for NodesType<'a, T> { 90 | type Target = NodeList<'a, T>; 91 | 92 | fn deref(&self) -> &Self::Target { 93 | &self.0 94 | } 95 | } 96 | 97 | impl DerefMut for NodesType<'_, T> { 98 | fn deref_mut(&mut self) -> &mut Self::Target { 99 | &mut self.0 100 | } 101 | } 102 | 103 | impl<'a, T: VariantValue> From> for NodesType<'a, T> { 104 | fn from(value: NodeList<'a, T>) -> Self { 105 | Self(value) 106 | } 107 | } 108 | 109 | impl<'a, T: VariantValue> From> for NodesType<'a, T> { 110 | fn from(values: Vec<&'a T>) -> Self { 111 | Self(NodeList::new(values)) 112 | } 113 | } 114 | 115 | /// SPath logical value. 116 | #[derive(Debug, Default, Clone, PartialEq, Eq)] 117 | pub enum LogicalType { 118 | /// True 119 | True, 120 | /// False 121 | #[default] 122 | False, 123 | } 124 | 125 | impl From for bool { 126 | fn from(value: LogicalType) -> Self { 127 | match value { 128 | LogicalType::True => true, 129 | LogicalType::False => false, 130 | } 131 | } 132 | } 133 | 134 | impl From for LogicalType { 135 | fn from(value: bool) -> Self { 136 | match value { 137 | true => Self::True, 138 | false => Self::False, 139 | } 140 | } 141 | } 142 | 143 | /// SPath value representing a singular value or Nothing. 144 | #[derive(Debug, Default, PartialEq, Eq, Clone)] 145 | pub enum ValueType<'a, T: VariantValue> { 146 | /// This may come from a literal value declared in an SPath query, or be produced by a 147 | /// function. 148 | Value(T), 149 | /// This would be a reference to a location in the object being queried, i.e., the result 150 | /// of a singular query, or produced by a function. 151 | Node(&'a T), 152 | /// This would be the result of a singular query that does not result in any nodes, or be 153 | /// produced by a function. 154 | #[default] 155 | Nothing, 156 | } 157 | 158 | impl ValueType<'_, T> { 159 | /// Convert to a reference of a variant value if possible. 160 | pub fn as_value(&self) -> Option<&T> { 161 | match self { 162 | ValueType::Value(v) => Some(v), 163 | ValueType::Node(v) => Some(*v), 164 | ValueType::Nothing => None, 165 | } 166 | } 167 | 168 | /// Check if this `ValueType` is nothing. 169 | pub fn is_nothing(&self) -> bool { 170 | matches!(self, ValueType::Nothing) 171 | } 172 | } 173 | 174 | impl From for ValueType<'_, T> { 175 | fn from(value: T) -> Self { 176 | Self::Value(value) 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /spath/src/spec/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 tison 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Types representing the concepts of RFC 9535. 16 | 17 | use crate::ConcreteVariantArray; 18 | use crate::ConcreteVariantObject; 19 | use crate::VariantValue; 20 | 21 | pub mod function; 22 | pub mod query; 23 | pub mod segment; 24 | pub mod selector; 25 | 26 | /// §2.3.2.2 (Wildcard Selector) Semantics 27 | /// 28 | /// A wildcard selector selects the nodes of all children of an object or array. 29 | /// 30 | /// Note that the children of an object are its member values, not its member names. 31 | /// 32 | /// The wildcard selector selects nothing from a primitive variant value. 33 | fn select_wildcard<'b, T: VariantValue>(result: &mut Vec<&'b T>, current: &'b T) { 34 | if let Some(list) = current.as_array() { 35 | for v in list.iter() { 36 | result.push(v); 37 | } 38 | } else if let Some(obj) = current.as_object() { 39 | for v in obj.values() { 40 | result.push(v); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /spath/src/spec/query.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 tison 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Types representing queries in SPath 16 | 17 | use std::fmt; 18 | 19 | use super::segment::QuerySegment; 20 | use crate::node::LocatedNode; 21 | use crate::path::NormalizedPath; 22 | use crate::spec::function::FunctionRegistry; 23 | use crate::VariantValue; 24 | 25 | mod sealed { 26 | use super::Query; 27 | use crate::spec::segment::QuerySegment; 28 | use crate::spec::segment::Segment; 29 | use crate::spec::selector::filter::Filter; 30 | use crate::spec::selector::index::Index; 31 | use crate::spec::selector::name::Name; 32 | use crate::spec::selector::slice::Slice; 33 | use crate::spec::selector::Selector; 34 | 35 | pub trait Sealed {} 36 | impl Sealed for Query {} 37 | impl Sealed for QuerySegment {} 38 | impl Sealed for Segment {} 39 | impl Sealed for Slice {} 40 | impl Sealed for Name {} 41 | impl Sealed for Selector {} 42 | impl Sealed for Index {} 43 | impl Sealed for Filter {} 44 | } 45 | 46 | /// A trait that can query a variant value. 47 | pub trait Queryable: sealed::Sealed { 48 | /// Run the query over a `current` node with a `root` node. 49 | fn query<'b, T: VariantValue, Registry: FunctionRegistry>( 50 | &self, 51 | current: &'b T, 52 | root: &'b T, 53 | registry: &Registry, 54 | ) -> Vec<&'b T>; 55 | 56 | /// Run the query over a `current` node with a `root` node and a `parent` path. 57 | fn query_located<'b, T: VariantValue, Registry: FunctionRegistry>( 58 | &self, 59 | current: &'b T, 60 | root: &'b T, 61 | registry: &Registry, 62 | parent: NormalizedPath<'b>, 63 | ) -> Vec>; 64 | } 65 | 66 | /// Represents an SPath expression 67 | #[derive(Debug, Clone, Default)] 68 | pub struct Query { 69 | /// The kind of query, root (`$`), or current (`@`) 70 | pub kind: QueryKind, 71 | /// The segments constituting the query 72 | pub segments: Vec, 73 | } 74 | 75 | impl Query { 76 | /// Whether this query extracts at most a singular node. 77 | pub fn is_singular(&self) -> bool { 78 | for s in &self.segments { 79 | if s.is_descendent() { 80 | return false; 81 | } 82 | if !s.segment.is_singular() { 83 | return false; 84 | } 85 | } 86 | true 87 | } 88 | } 89 | 90 | impl fmt::Display for Query { 91 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 92 | match self.kind { 93 | QueryKind::Root => write!(f, "$")?, 94 | QueryKind::Current => write!(f, "@")?, 95 | } 96 | for s in &self.segments { 97 | write!(f, "{s}")?; 98 | } 99 | Ok(()) 100 | } 101 | } 102 | 103 | /// The kind of query 104 | #[derive(Debug, PartialEq, Eq, Clone, Default)] 105 | pub enum QueryKind { 106 | /// A query against the root of a variant object, i.e., with `$` 107 | #[default] 108 | Root, 109 | /// A query against the current node within a variant object, i.e., with `@` 110 | Current, 111 | } 112 | 113 | impl Queryable for Query { 114 | fn query<'b, T: VariantValue, Registry: FunctionRegistry>( 115 | &self, 116 | current: &'b T, 117 | root: &'b T, 118 | registry: &Registry, 119 | ) -> Vec<&'b T> { 120 | let mut result = match self.kind { 121 | QueryKind::Root => vec![root], 122 | QueryKind::Current => vec![current], 123 | }; 124 | for segment in &self.segments { 125 | let mut r = Vec::new(); 126 | for node in result { 127 | r.append(&mut segment.query(node, root, registry)); 128 | } 129 | result = r; 130 | } 131 | result 132 | } 133 | 134 | fn query_located<'b, T: VariantValue, Registry: FunctionRegistry>( 135 | &self, 136 | current: &'b T, 137 | root: &'b T, 138 | registry: &Registry, 139 | parent: NormalizedPath<'b>, 140 | ) -> Vec> { 141 | let mut result = match self.kind { 142 | QueryKind::Current => vec![LocatedNode::new(parent, current)], 143 | QueryKind::Root => vec![LocatedNode::new(Default::default(), root)], 144 | }; 145 | for s in &self.segments { 146 | let mut r = vec![]; 147 | for n in result { 148 | let loc = n.location(); 149 | let node = n.node(); 150 | r.append(&mut s.query_located(node, root, registry, loc.clone())); 151 | } 152 | result = r; 153 | } 154 | result 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /spath/src/spec/segment.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 tison 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::fmt; 16 | 17 | use crate::spec::function::FunctionRegistry; 18 | use crate::spec::query::Queryable; 19 | use crate::spec::select_wildcard; 20 | use crate::spec::selector::Selector; 21 | use crate::ConcreteVariantArray; 22 | use crate::ConcreteVariantObject; 23 | use crate::LocatedNode; 24 | use crate::NormalizedPath; 25 | use crate::VariantValue; 26 | 27 | /// A segment of a SPath query 28 | #[derive(Debug, Clone)] 29 | pub struct QuerySegment { 30 | /// The kind of segment 31 | pub kind: QuerySegmentKind, 32 | /// The segment 33 | pub segment: Segment, 34 | } 35 | 36 | impl QuerySegment { 37 | /// Is this a normal child segment 38 | pub fn is_child(&self) -> bool { 39 | matches!(self.kind, QuerySegmentKind::Child) 40 | } 41 | 42 | /// Is this a recursive descent child 43 | pub fn is_descendent(&self) -> bool { 44 | !self.is_child() 45 | } 46 | } 47 | 48 | impl fmt::Display for QuerySegment { 49 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 50 | if matches!(self.kind, QuerySegmentKind::Descendant) { 51 | write!(f, "..")?; 52 | } 53 | write!(f, "{}", self.segment) 54 | } 55 | } 56 | 57 | impl Queryable for QuerySegment { 58 | fn query<'b, T: VariantValue, Registry: FunctionRegistry>( 59 | &self, 60 | current: &'b T, 61 | root: &'b T, 62 | registry: &Registry, 63 | ) -> Vec<&'b T> { 64 | let mut query = self.segment.query(current, root, registry); 65 | if matches!(self.kind, QuerySegmentKind::Descendant) { 66 | query.append(&mut descend(self, current, root, registry)); 67 | } 68 | query 69 | } 70 | 71 | fn query_located<'b, T: VariantValue, Registry: FunctionRegistry>( 72 | &self, 73 | current: &'b T, 74 | root: &'b T, 75 | registry: &Registry, 76 | parent: NormalizedPath<'b>, 77 | ) -> Vec> { 78 | if matches!(self.kind, QuerySegmentKind::Descendant) { 79 | let mut result = self 80 | .segment 81 | .query_located(current, root, registry, parent.clone()); 82 | result.append(&mut descend_paths(self, current, root, registry, parent)); 83 | result 84 | } else { 85 | self.segment.query_located(current, root, registry, parent) 86 | } 87 | } 88 | } 89 | 90 | fn descend<'b, T: VariantValue, Registry: FunctionRegistry>( 91 | segment: &QuerySegment, 92 | current: &'b T, 93 | root: &'b T, 94 | registry: &Registry, 95 | ) -> Vec<&'b T> { 96 | let mut query = Vec::new(); 97 | if let Some(list) = current.as_array() { 98 | for v in list.iter() { 99 | query.append(&mut segment.query(v, root, registry)); 100 | } 101 | } else if let Some(obj) = current.as_object() { 102 | for v in obj.values() { 103 | query.append(&mut segment.query(v, root, registry)); 104 | } 105 | } 106 | query 107 | } 108 | 109 | fn descend_paths<'b, T: VariantValue, Registry: FunctionRegistry>( 110 | segment: &QuerySegment, 111 | current: &'b T, 112 | root: &'b T, 113 | registry: &Registry, 114 | parent: NormalizedPath<'b>, 115 | ) -> Vec> { 116 | let mut result = Vec::new(); 117 | if let Some(list) = current.as_array() { 118 | for (i, v) in list.iter().enumerate() { 119 | result.append(&mut segment.query_located(v, root, registry, parent.clone_and_push(i))); 120 | } 121 | } else if let Some(obj) = current.as_object() { 122 | for (k, v) in obj.iter() { 123 | result.append(&mut segment.query_located(v, root, registry, parent.clone_and_push(k))); 124 | } 125 | } 126 | result 127 | } 128 | 129 | /// The kind of query segment 130 | #[derive(Debug, PartialEq, Eq, Clone)] 131 | pub enum QuerySegmentKind { 132 | /// A normal child 133 | /// 134 | /// Addresses the direct descendant of the preceding segment 135 | Child, 136 | /// A descendant child 137 | /// 138 | /// Addresses all descendant children of the preceding segment, recursively 139 | Descendant, 140 | } 141 | 142 | /// Represents the different forms of SPath segment. 143 | #[derive(Debug, Clone)] 144 | pub enum Segment { 145 | /// Long hand segments contain multiple selectors inside square brackets. 146 | LongHand(Vec), 147 | /// Dot-name selectors are a short form for representing keys in an object. 148 | DotName(String), 149 | /// The wildcard shorthand `.*`. 150 | Wildcard, 151 | } 152 | 153 | impl Segment { 154 | /// Whether this segment extracts at most a singular node. 155 | pub fn is_singular(&self) -> bool { 156 | match self { 157 | Segment::LongHand(selectors) => { 158 | if selectors.len() > 1 { 159 | return false; 160 | } 161 | if let Some(s) = selectors.first() { 162 | s.is_singular() 163 | } else { 164 | // if the selector list is empty, this shouldn't be a valid 165 | // SPath, but at least, it would be selecting nothing, and 166 | // that could be considered singular, i.e., None. 167 | true 168 | } 169 | } 170 | Segment::DotName(_) => true, 171 | Segment::Wildcard => false, 172 | } 173 | } 174 | 175 | /// Optionally produce self as a slice of selectors, from a long hand segment. 176 | pub fn as_long_hand(&self) -> Option<&[Selector]> { 177 | match self { 178 | Segment::LongHand(v) => Some(v.as_slice()), 179 | _ => None, 180 | } 181 | } 182 | 183 | /// Optionally produce self as a single name segment. 184 | pub fn as_dot_name(&self) -> Option<&str> { 185 | match self { 186 | Segment::DotName(s) => Some(s.as_str()), 187 | _ => None, 188 | } 189 | } 190 | } 191 | 192 | impl fmt::Display for Segment { 193 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 194 | match self { 195 | Segment::LongHand(selectors) => { 196 | write!(f, "[")?; 197 | for (i, s) in selectors.iter().enumerate() { 198 | write!( 199 | f, 200 | "{s}{comma}", 201 | comma = if i == selectors.len() - 1 { "" } else { "," } 202 | )?; 203 | } 204 | write!(f, "]")?; 205 | } 206 | Segment::DotName(name) => write!(f, ".{name}")?, 207 | Segment::Wildcard => write!(f, ".*")?, 208 | } 209 | Ok(()) 210 | } 211 | } 212 | 213 | impl Queryable for Segment { 214 | fn query<'b, T: VariantValue, Registry: FunctionRegistry>( 215 | &self, 216 | current: &'b T, 217 | root: &'b T, 218 | registry: &Registry, 219 | ) -> Vec<&'b T> { 220 | let mut result = Vec::new(); 221 | match self { 222 | Segment::LongHand(selectors) => { 223 | for selector in selectors { 224 | result.append(&mut selector.query(current, root, registry)); 225 | } 226 | } 227 | Segment::DotName(key) => { 228 | if let Some(obj) = current.as_object() { 229 | if let Some(v) = obj.get(key) { 230 | result.push(v); 231 | } 232 | } 233 | } 234 | Segment::Wildcard => select_wildcard(&mut result, current), 235 | } 236 | result 237 | } 238 | 239 | fn query_located<'b, T: VariantValue, Registry: FunctionRegistry>( 240 | &self, 241 | current: &'b T, 242 | root: &'b T, 243 | registry: &Registry, 244 | mut parent: NormalizedPath<'b>, 245 | ) -> Vec> { 246 | let mut result = vec![]; 247 | match self { 248 | Segment::LongHand(selectors) => { 249 | for s in selectors { 250 | result.append(&mut s.query_located(current, root, registry, parent.clone())); 251 | } 252 | } 253 | Segment::DotName(name) => { 254 | if let Some((k, v)) = current.as_object().and_then(|o| o.get_key_value(name)) { 255 | parent.push(k); 256 | result.push(LocatedNode::new(parent, v)); 257 | } 258 | } 259 | Segment::Wildcard => { 260 | if let Some(list) = current.as_array() { 261 | for (i, v) in list.iter().enumerate() { 262 | result.push(LocatedNode::new(parent.clone_and_push(i), v)); 263 | } 264 | } else if let Some(obj) = current.as_object() { 265 | for (k, v) in obj.iter() { 266 | result.push(LocatedNode::new(parent.clone_and_push(k), v)); 267 | } 268 | } 269 | } 270 | } 271 | result 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /spath/src/spec/selector/filter.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 tison 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Types representing filter selectors in SPath. 16 | 17 | use std::fmt; 18 | 19 | use super::index::Index; 20 | use super::name::Name; 21 | use super::Selector; 22 | use crate::node::LocatedNode; 23 | use crate::path::NormalizedPath; 24 | use crate::spec::function::FunctionExpr; 25 | use crate::spec::function::FunctionRegistry; 26 | use crate::spec::function::SPathValue; 27 | use crate::spec::query::Query; 28 | use crate::spec::query::QueryKind; 29 | use crate::spec::query::Queryable; 30 | use crate::spec::segment::QuerySegment; 31 | use crate::spec::segment::Segment; 32 | use crate::ConcreteVariantArray; 33 | use crate::ConcreteVariantObject; 34 | use crate::Literal; 35 | use crate::VariantValue; 36 | 37 | mod sealed { 38 | use super::BasicExpr; 39 | use super::ComparisonExpr; 40 | use super::ExistExpr; 41 | use super::LogicalAndExpr; 42 | use super::LogicalOrExpr; 43 | use crate::spec::function::FunctionExpr; 44 | 45 | pub trait Sealed {} 46 | impl Sealed for LogicalOrExpr {} 47 | impl Sealed for LogicalAndExpr {} 48 | impl Sealed for BasicExpr {} 49 | impl Sealed for ExistExpr {} 50 | impl Sealed for ComparisonExpr {} 51 | impl Sealed for FunctionExpr {} 52 | } 53 | 54 | /// Trait for testing a filter type. 55 | pub trait TestFilter: sealed::Sealed { 56 | /// Test self using the current and root nodes. 57 | fn test_filter<'b, T: VariantValue, Registry: FunctionRegistry>( 58 | &self, 59 | current: &'b T, 60 | root: &'b T, 61 | registry: &Registry, 62 | ) -> bool; 63 | } 64 | 65 | /// The main filter type for SPath. 66 | #[derive(Debug, Clone)] 67 | pub struct Filter(pub LogicalOrExpr); 68 | 69 | impl fmt::Display for Filter { 70 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 71 | write!(f, "{expr}", expr = self.0) 72 | } 73 | } 74 | 75 | impl Queryable for Filter { 76 | fn query<'b, T: VariantValue, Registry: FunctionRegistry>( 77 | &self, 78 | current: &'b T, 79 | root: &'b T, 80 | registry: &Registry, 81 | ) -> Vec<&'b T> { 82 | if let Some(list) = current.as_array() { 83 | list.iter() 84 | .filter(|v| self.0.test_filter(*v, root, registry)) 85 | .collect() 86 | } else if let Some(obj) = current.as_object() { 87 | obj.iter() 88 | .map(|(_, v)| v) 89 | .filter(|v| self.0.test_filter(*v, root, registry)) 90 | .collect() 91 | } else { 92 | vec![] 93 | } 94 | } 95 | 96 | fn query_located<'b, T: VariantValue, Registry: FunctionRegistry>( 97 | &self, 98 | current: &'b T, 99 | root: &'b T, 100 | registry: &Registry, 101 | parent: NormalizedPath<'b>, 102 | ) -> Vec> { 103 | if let Some(list) = current.as_array() { 104 | list.iter() 105 | .enumerate() 106 | .filter(|(_, v)| self.0.test_filter(*v, root, registry)) 107 | .map(|(i, v)| LocatedNode::new(parent.clone_and_push(i), v)) 108 | .collect() 109 | } else if let Some(obj) = current.as_object() { 110 | obj.iter() 111 | .filter(|(_, v)| self.0.test_filter(*v, root, registry)) 112 | .map(|(k, v)| LocatedNode::new(parent.clone_and_push(k), v)) 113 | .collect() 114 | } else { 115 | vec![] 116 | } 117 | } 118 | } 119 | 120 | /// The top level boolean expression type. 121 | /// 122 | /// This is also `logical-expression` in RFC 9535, but the naming was chosen to 123 | /// make it more clear that it represents the logical OR, and to not have an extra wrapping type. 124 | #[derive(Debug, Clone)] 125 | pub struct LogicalOrExpr(pub Vec); 126 | 127 | impl fmt::Display for LogicalOrExpr { 128 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 129 | for (i, expr) in self.0.iter().enumerate() { 130 | write!( 131 | f, 132 | "{expr}{logic}", 133 | logic = if i == self.0.len() - 1 { "" } else { " || " } 134 | )?; 135 | } 136 | Ok(()) 137 | } 138 | } 139 | 140 | impl TestFilter for LogicalOrExpr { 141 | fn test_filter<'b, T: VariantValue, Registry: FunctionRegistry>( 142 | &self, 143 | current: &'b T, 144 | root: &'b T, 145 | registry: &Registry, 146 | ) -> bool { 147 | self.0 148 | .iter() 149 | .any(|expr| expr.test_filter(current, root, registry)) 150 | } 151 | } 152 | 153 | /// A logical AND expression. 154 | #[derive(Debug, Clone)] 155 | pub struct LogicalAndExpr(pub Vec); 156 | 157 | impl fmt::Display for LogicalAndExpr { 158 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 159 | for (i, expr) in self.0.iter().enumerate() { 160 | write!( 161 | f, 162 | "{expr}{logic}", 163 | logic = if i == self.0.len() - 1 { "" } else { " && " } 164 | )?; 165 | } 166 | Ok(()) 167 | } 168 | } 169 | 170 | impl TestFilter for LogicalAndExpr { 171 | fn test_filter<'b, T: VariantValue, Registry: FunctionRegistry>( 172 | &self, 173 | current: &'b T, 174 | root: &'b T, 175 | registry: &Registry, 176 | ) -> bool { 177 | self.0 178 | .iter() 179 | .all(|expr| expr.test_filter(current, root, registry)) 180 | } 181 | } 182 | 183 | /// The basic for m of expression in a filter. 184 | #[derive(Debug, Clone)] 185 | pub enum BasicExpr { 186 | /// An expression wrapped in parentheses. 187 | Paren(LogicalOrExpr), 188 | /// A parenthesized expression preceded with a `!`. 189 | ParenNot(LogicalOrExpr), 190 | /// A relationship expression which compares two variant values. 191 | Relation(ComparisonExpr), 192 | /// An existence expression. 193 | Exist(ExistExpr), 194 | /// The inverse of an existence expression, i.e., preceded by `!`. 195 | NotExist(ExistExpr), 196 | /// A function expression. 197 | FuncExpr(FunctionExpr), 198 | /// The inverse of a function expression, i.e., preceded by `!`. 199 | FuncNotExpr(FunctionExpr), 200 | } 201 | 202 | impl fmt::Display for BasicExpr { 203 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 204 | match self { 205 | BasicExpr::Paren(expr) => write!(f, "({expr})"), 206 | BasicExpr::ParenNot(expr) => write!(f, "!({expr})"), 207 | BasicExpr::Relation(rel) => write!(f, "{rel}"), 208 | BasicExpr::Exist(exist) => write!(f, "{exist}"), 209 | BasicExpr::NotExist(exist) => write!(f, "!{exist}"), 210 | BasicExpr::FuncExpr(expr) => write!(f, "{expr}"), 211 | BasicExpr::FuncNotExpr(expr) => write!(f, "!{expr}"), 212 | } 213 | } 214 | } 215 | 216 | impl BasicExpr { 217 | /// Optionally express as a relation expression 218 | pub fn as_relation(&self) -> Option<&ComparisonExpr> { 219 | match self { 220 | BasicExpr::Relation(cx) => Some(cx), 221 | _ => None, 222 | } 223 | } 224 | } 225 | 226 | impl TestFilter for BasicExpr { 227 | fn test_filter<'b, T: VariantValue, Registry: FunctionRegistry>( 228 | &self, 229 | current: &'b T, 230 | root: &'b T, 231 | registry: &Registry, 232 | ) -> bool { 233 | match self { 234 | BasicExpr::Paren(expr) => expr.test_filter(current, root, registry), 235 | BasicExpr::ParenNot(expr) => !expr.test_filter(current, root, registry), 236 | BasicExpr::Relation(expr) => expr.test_filter(current, root, registry), 237 | BasicExpr::Exist(expr) => expr.test_filter(current, root, registry), 238 | BasicExpr::NotExist(expr) => !expr.test_filter(current, root, registry), 239 | BasicExpr::FuncExpr(expr) => expr.test_filter(current, root, registry), 240 | BasicExpr::FuncNotExpr(expr) => !expr.test_filter(current, root, registry), 241 | } 242 | } 243 | } 244 | 245 | /// Existence expression. 246 | #[derive(Debug, Clone)] 247 | pub struct ExistExpr(pub Query); 248 | 249 | impl fmt::Display for ExistExpr { 250 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 251 | write!(f, "{query}", query = self.0) 252 | } 253 | } 254 | 255 | impl TestFilter for ExistExpr { 256 | fn test_filter<'b, T: VariantValue, Registry: FunctionRegistry>( 257 | &self, 258 | current: &'b T, 259 | root: &'b T, 260 | registry: &Registry, 261 | ) -> bool { 262 | !self.0.query(current, root, registry).is_empty() 263 | } 264 | } 265 | 266 | /// A comparison expression comparing two variant values 267 | #[derive(Debug, Clone)] 268 | pub struct ComparisonExpr { 269 | /// The variant value on the left of the comparison 270 | pub left: Comparable, 271 | /// The operator of comparison 272 | pub op: ComparisonOperator, 273 | /// The variant value on the right of the comparison 274 | pub right: Comparable, 275 | } 276 | 277 | impl fmt::Display for ComparisonExpr { 278 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 279 | write!( 280 | f, 281 | "{left}{op}{right}", 282 | left = self.left, 283 | op = self.op, 284 | right = self.right 285 | ) 286 | } 287 | } 288 | 289 | impl TestFilter for ComparisonExpr { 290 | fn test_filter<'b, T: VariantValue, Registry: FunctionRegistry>( 291 | &self, 292 | current: &'b T, 293 | root: &'b T, 294 | registry: &Registry, 295 | ) -> bool { 296 | let left = self.left.as_value(current, root, registry); 297 | let right = self.right.as_value(current, root, registry); 298 | match self.op { 299 | ComparisonOperator::EqualTo => check_equal_to(&left, &right), 300 | ComparisonOperator::NotEqualTo => !check_equal_to(&left, &right), 301 | ComparisonOperator::LessThan => check_less_than(&left, &right), 302 | ComparisonOperator::GreaterThan => check_less_than(&right, &left), 303 | ComparisonOperator::LessThanEqualTo => { 304 | check_less_than(&left, &right) || check_equal_to(&left, &right) 305 | } 306 | ComparisonOperator::GreaterThanEqualTo => { 307 | check_less_than(&right, &left) || check_equal_to(&left, &right) 308 | } 309 | } 310 | } 311 | } 312 | 313 | fn check_equal_to(left: &SPathValue, right: &SPathValue) -> bool { 314 | let (left, right) = match (left, right) { 315 | (SPathValue::Node(v1), SPathValue::Node(v2)) => (*v1, *v2), 316 | (SPathValue::Node(v1), SPathValue::Value(v2)) => (*v1, v2), 317 | (SPathValue::Value(v1), SPathValue::Node(v2)) => (v1, *v2), 318 | (SPathValue::Value(v1), SPathValue::Value(v2)) => (v1, v2), 319 | (SPathValue::Nothing, SPathValue::Nothing) => return true, 320 | (SPathValue::Nodes(l1), SPathValue::Nodes(l2)) => return l1.is_empty() && l2.is_empty(), 321 | _ => return false, 322 | }; 323 | 324 | left.is_equal_to(right) 325 | } 326 | 327 | fn check_less_than(left: &SPathValue, right: &SPathValue) -> bool { 328 | let (left, right) = match (left, right) { 329 | (SPathValue::Node(v1), SPathValue::Node(v2)) => (*v1, *v2), 330 | (SPathValue::Node(v1), SPathValue::Value(v2)) => (*v1, v2), 331 | (SPathValue::Value(v1), SPathValue::Node(v2)) => (v1, *v2), 332 | (SPathValue::Value(v1), SPathValue::Value(v2)) => (v1, v2), 333 | _ => return false, 334 | }; 335 | 336 | left.is_less_than(right) 337 | } 338 | 339 | /// The comparison operator 340 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 341 | pub enum ComparisonOperator { 342 | /// `==` 343 | EqualTo, 344 | /// `!=` 345 | NotEqualTo, 346 | /// `<` 347 | LessThan, 348 | /// `>` 349 | GreaterThan, 350 | /// `<=` 351 | LessThanEqualTo, 352 | /// `>=` 353 | GreaterThanEqualTo, 354 | } 355 | 356 | impl fmt::Display for ComparisonOperator { 357 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 358 | match self { 359 | ComparisonOperator::EqualTo => write!(f, "=="), 360 | ComparisonOperator::NotEqualTo => write!(f, "!="), 361 | ComparisonOperator::LessThan => write!(f, "<"), 362 | ComparisonOperator::GreaterThan => write!(f, ">"), 363 | ComparisonOperator::LessThanEqualTo => write!(f, "<="), 364 | ComparisonOperator::GreaterThanEqualTo => write!(f, ">="), 365 | } 366 | } 367 | } 368 | 369 | /// A type that is comparable 370 | #[derive(Debug, Clone)] 371 | pub enum Comparable { 372 | /// A literal variant value, excluding objects and arrays. 373 | Literal(Literal), 374 | /// A singular query. 375 | /// 376 | /// This will only produce a single node, i.e., a variant value, or nothing 377 | SingularQuery(SingularQuery), 378 | /// A function expression that can only produce a `ValueType` 379 | FunctionExpr(FunctionExpr), 380 | } 381 | 382 | impl fmt::Display for Comparable { 383 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 384 | match self { 385 | Comparable::Literal(lit) => write!(f, "{lit}"), 386 | Comparable::SingularQuery(path) => write!(f, "{path}"), 387 | Comparable::FunctionExpr(expr) => write!(f, "{expr}"), 388 | } 389 | } 390 | } 391 | 392 | impl Comparable { 393 | /// Convert the comparable variable to a variant value. 394 | pub fn as_value<'a, 'b: 'a, T: VariantValue, Registry: FunctionRegistry>( 395 | &'a self, 396 | current: &'b T, 397 | root: &'b T, 398 | registry: &Registry, 399 | ) -> SPathValue<'a, T> { 400 | match self { 401 | Comparable::Literal(lit) => match T::from_literal(lit.clone()) { 402 | Some(v) => SPathValue::Value(v), 403 | None => SPathValue::Nothing, 404 | }, 405 | Comparable::SingularQuery(sp) => match sp.eval_query(current, root) { 406 | Some(v) => SPathValue::Node(v), 407 | None => SPathValue::Nothing, 408 | }, 409 | Comparable::FunctionExpr(expr) => expr.evaluate(current, root, registry), 410 | } 411 | } 412 | } 413 | 414 | /// A segment in a singular query 415 | #[derive(Debug, PartialEq, Eq, Clone)] 416 | pub enum SingularQuerySegment { 417 | /// A single name segment 418 | Name(Name), 419 | /// A single index segment 420 | Index(Index), 421 | } 422 | 423 | impl fmt::Display for SingularQuerySegment { 424 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 425 | match self { 426 | SingularQuerySegment::Name(name) => write!(f, "{name}"), 427 | SingularQuerySegment::Index(index) => write!(f, "{index}"), 428 | } 429 | } 430 | } 431 | 432 | impl TryFrom for SingularQuerySegment { 433 | type Error = NonSingularQueryError; 434 | 435 | fn try_from(segment: QuerySegment) -> Result { 436 | if segment.is_descendent() { 437 | return Err(NonSingularQueryError::Descendant); 438 | } 439 | match segment.segment { 440 | Segment::LongHand(mut selectors) => { 441 | if selectors.len() > 1 { 442 | Err(NonSingularQueryError::TooManySelectors) 443 | } else if let Some(sel) = selectors.pop() { 444 | sel.try_into() 445 | } else { 446 | Err(NonSingularQueryError::NoSelectors) 447 | } 448 | } 449 | Segment::DotName(name) => Ok(Self::Name(Name::new(name))), 450 | Segment::Wildcard => Err(NonSingularQueryError::Wildcard), 451 | } 452 | } 453 | } 454 | 455 | impl TryFrom for SingularQuerySegment { 456 | type Error = NonSingularQueryError; 457 | 458 | fn try_from(selector: Selector) -> Result { 459 | match selector { 460 | Selector::Name(n) => Ok(Self::Name(n)), 461 | Selector::Wildcard => Err(NonSingularQueryError::Wildcard), 462 | Selector::Index(i) => Ok(Self::Index(i)), 463 | Selector::ArraySlice(_) => Err(NonSingularQueryError::Slice), 464 | Selector::Filter(_) => Err(NonSingularQueryError::Filter), 465 | } 466 | } 467 | } 468 | 469 | /// Represents a singular query in SPath 470 | #[derive(Debug, PartialEq, Eq, Clone)] 471 | pub struct SingularQuery { 472 | /// The kind of singular query, relative or absolute 473 | pub kind: SingularQueryKind, 474 | /// The segments making up the query 475 | pub segments: Vec, 476 | } 477 | 478 | impl SingularQuery { 479 | /// Evaluate the singular query 480 | pub fn eval_query<'b, T: VariantValue>(&self, current: &'b T, root: &'b T) -> Option<&'b T> { 481 | let mut target = match self.kind { 482 | SingularQueryKind::Absolute => root, 483 | SingularQueryKind::Relative => current, 484 | }; 485 | for segment in &self.segments { 486 | match segment { 487 | SingularQuerySegment::Name(name) => { 488 | if let Some(t) = target.as_object().and_then(|o| o.get(name.as_str())) { 489 | target = t; 490 | } else { 491 | return None; 492 | } 493 | } 494 | SingularQuerySegment::Index(i) => { 495 | if let Some(t) = target 496 | .as_array() 497 | .and_then(|l| usize::try_from(i.index()).ok().and_then(|i| l.get(i))) 498 | { 499 | target = t; 500 | } else { 501 | return None; 502 | } 503 | } 504 | } 505 | } 506 | Some(target) 507 | } 508 | } 509 | 510 | impl TryFrom for SingularQuery { 511 | type Error = NonSingularQueryError; 512 | 513 | fn try_from(query: Query) -> Result { 514 | let kind = SingularQueryKind::from(query.kind); 515 | let segments = query 516 | .segments 517 | .into_iter() 518 | .map(TryFrom::try_from) 519 | .collect::, Self::Error>>()?; 520 | Ok(Self { kind, segments }) 521 | } 522 | } 523 | 524 | impl fmt::Display for SingularQuery { 525 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 526 | match self.kind { 527 | SingularQueryKind::Absolute => write!(f, "$")?, 528 | SingularQueryKind::Relative => write!(f, "@")?, 529 | } 530 | for s in &self.segments { 531 | write!(f, "[{s}]")?; 532 | } 533 | Ok(()) 534 | } 535 | } 536 | 537 | /// The kind of singular query 538 | #[derive(Debug, PartialEq, Eq, Clone, Copy)] 539 | pub enum SingularQueryKind { 540 | /// Referencing the root node, i.e., `$` 541 | Absolute, 542 | /// Referencing the current node, i.e., `@` 543 | Relative, 544 | } 545 | 546 | impl From for SingularQueryKind { 547 | fn from(qk: QueryKind) -> Self { 548 | match qk { 549 | QueryKind::Root => Self::Absolute, 550 | QueryKind::Current => Self::Relative, 551 | } 552 | } 553 | } 554 | 555 | /// Error when parsing a singular query 556 | #[derive(Debug, thiserror::Error, PartialEq)] 557 | pub enum NonSingularQueryError { 558 | /// Descendant segment 559 | #[error("descendant segments are not singular")] 560 | Descendant, 561 | /// Long hand segment with too many internal selectors 562 | #[error("long hand segment contained more than one selector")] 563 | TooManySelectors, 564 | /// Long hand segment with no selectors 565 | #[error("long hand segment contained no selectors")] 566 | NoSelectors, 567 | /// A wildcard segment 568 | #[error("wildcard segments are not singular")] 569 | Wildcard, 570 | /// A slice segment 571 | #[error("slice segments are not singular")] 572 | Slice, 573 | /// A filter segment 574 | #[error("filter segments are not singular")] 575 | Filter, 576 | } 577 | -------------------------------------------------------------------------------- /spath/src/spec/selector/index.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 tison 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Index selectors in SPath. 16 | 17 | use std::fmt; 18 | 19 | use num_traits::ToPrimitive; 20 | 21 | use crate::spec::function::FunctionRegistry; 22 | use crate::spec::query::Queryable; 23 | use crate::ConcreteVariantArray; 24 | use crate::LocatedNode; 25 | use crate::NormalizedPath; 26 | use crate::VariantValue; 27 | 28 | /// §2.3.3 Index Selector. 29 | /// 30 | /// For selecting array elements by their index. 31 | /// 32 | /// Can use negative indices to index from the end of an array. 33 | #[derive(Debug, PartialEq, Eq, Clone, Copy)] 34 | pub struct Index { 35 | /// The index of the selector. 36 | index: i64, 37 | } 38 | 39 | impl Index { 40 | /// Create a new index selector. 41 | pub fn new(index: i64) -> Self { 42 | Self { index } 43 | } 44 | 45 | /// Get the index of the selector. 46 | pub fn index(&self) -> i64 { 47 | self.index 48 | } 49 | } 50 | 51 | impl fmt::Display for Index { 52 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 53 | write!(f, "{}", self.index) 54 | } 55 | } 56 | 57 | // §2.3.3.2. (Index Selector) Semantics 58 | fn resolve_index(index: i64, len: usize) -> Option { 59 | let index = if index >= 0 { 60 | index.to_usize()? 61 | } else { 62 | let index = len.to_i64().unwrap_or(i64::MAX) + index; 63 | index.to_usize()? 64 | }; 65 | 66 | if index < len { 67 | Some(index) 68 | } else { 69 | None 70 | } 71 | } 72 | 73 | impl Queryable for Index { 74 | fn query<'b, T: VariantValue, Registry: FunctionRegistry>( 75 | &self, 76 | current: &'b T, 77 | _root: &'b T, 78 | _registry: &Registry, 79 | ) -> Vec<&'b T> { 80 | current 81 | .as_array() 82 | .and_then(|list| { 83 | let index = resolve_index(self.index, list.len())?; 84 | list.get(index) 85 | }) 86 | .map(|node| vec![node]) 87 | .unwrap_or_default() 88 | } 89 | 90 | fn query_located<'b, T: VariantValue, Registry: FunctionRegistry>( 91 | &self, 92 | current: &'b T, 93 | _root: &'b T, 94 | _registry: &Registry, 95 | mut parent: NormalizedPath<'b>, 96 | ) -> Vec> { 97 | current 98 | .as_array() 99 | .and_then(|list| { 100 | let index = resolve_index(self.index, list.len())?; 101 | list.get(index).map(|node| (index, node)) 102 | }) 103 | .map(|(i, node)| { 104 | parent.push(i); 105 | vec![LocatedNode::new(parent, node)] 106 | }) 107 | .unwrap_or_default() 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /spath/src/spec/selector/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 tison 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Types representing the different selectors in SPath. 16 | 17 | pub mod filter; 18 | pub mod index; 19 | pub mod name; 20 | pub mod slice; 21 | 22 | use std::fmt; 23 | 24 | use self::index::Index; 25 | use self::name::Name; 26 | use self::slice::Slice; 27 | use crate::spec::function::FunctionRegistry; 28 | use crate::spec::query::Queryable; 29 | use crate::spec::select_wildcard; 30 | use crate::spec::selector::filter::Filter; 31 | use crate::ConcreteVariantArray; 32 | use crate::ConcreteVariantObject; 33 | use crate::LocatedNode; 34 | use crate::NormalizedPath; 35 | use crate::VariantValue; 36 | 37 | /// An SPath selector 38 | #[derive(Debug, Clone)] 39 | pub enum Selector { 40 | /// Select an object key 41 | Name(Name), 42 | /// Select all nodes 43 | /// 44 | /// For an object, this produces a nodelist of all member values; for an array, this produces a 45 | /// nodelist of all array elements. 46 | Wildcard, 47 | /// Select an array element 48 | Index(Index), 49 | /// Select a slice from an array 50 | ArraySlice(Slice), 51 | /// Use a filter to select nodes 52 | Filter(Filter), 53 | } 54 | 55 | impl Selector { 56 | /// Whether this selector selects at most a single node. 57 | pub fn is_singular(&self) -> bool { 58 | matches!(self, Selector::Name(_) | Selector::Index(_)) 59 | } 60 | } 61 | 62 | impl fmt::Display for Selector { 63 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 64 | match self { 65 | Selector::Name(name) => write!(f, "{name}"), 66 | Selector::Wildcard => write!(f, "*"), 67 | Selector::Index(index) => write!(f, "{index}"), 68 | Selector::ArraySlice(slice) => write!(f, "{slice}"), 69 | Selector::Filter(filter) => write!(f, "?{filter}"), 70 | } 71 | } 72 | } 73 | 74 | impl Queryable for Selector { 75 | fn query<'b, T: VariantValue, Registry: FunctionRegistry>( 76 | &self, 77 | current: &'b T, 78 | root: &'b T, 79 | registry: &Registry, 80 | ) -> Vec<&'b T> { 81 | let mut result = Vec::new(); 82 | match self { 83 | Selector::Name(name) => result.append(&mut name.query(current, root, registry)), 84 | Selector::Wildcard => select_wildcard(&mut result, current), 85 | Selector::Index(index) => result.append(&mut index.query(current, root, registry)), 86 | Selector::ArraySlice(slice) => result.append(&mut slice.query(current, root, registry)), 87 | Selector::Filter(filter) => result.append(&mut filter.query(current, root, registry)), 88 | } 89 | result 90 | } 91 | 92 | fn query_located<'b, T: VariantValue, Registry: FunctionRegistry>( 93 | &self, 94 | current: &'b T, 95 | root: &'b T, 96 | registry: &Registry, 97 | parent: NormalizedPath<'b>, 98 | ) -> Vec> { 99 | match self { 100 | Selector::Name(name) => name.query_located(current, root, registry, parent), 101 | Selector::Wildcard => { 102 | if let Some(list) = current.as_array() { 103 | list.iter() 104 | .enumerate() 105 | .map(|(i, node)| LocatedNode::new(parent.clone_and_push(i), node)) 106 | .collect() 107 | } else if let Some(obj) = current.as_object() { 108 | obj.iter() 109 | .map(|(k, node)| LocatedNode::new(parent.clone_and_push(k), node)) 110 | .collect() 111 | } else { 112 | vec![] 113 | } 114 | } 115 | Selector::Index(index) => index.query_located(current, root, registry, parent), 116 | Selector::ArraySlice(slice) => slice.query_located(current, root, registry, parent), 117 | Selector::Filter(filter) => filter.query_located(current, root, registry, parent), 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /spath/src/spec/selector/name.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 tison 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Name selectors for selecting object keys in SPath. 16 | 17 | use std::fmt; 18 | 19 | use crate::spec::function::FunctionRegistry; 20 | use crate::spec::query::Queryable; 21 | use crate::ConcreteVariantObject; 22 | use crate::LocatedNode; 23 | use crate::NormalizedPath; 24 | use crate::VariantValue; 25 | 26 | /// §2.3.1 Name Selector. 27 | /// 28 | /// Select a single variant object key. 29 | #[derive(Debug, PartialEq, Eq, Clone)] 30 | pub struct Name { 31 | name: String, 32 | } 33 | 34 | impl Name { 35 | /// Create a new name selector. 36 | pub fn new(name: String) -> Self { 37 | Self { name } 38 | } 39 | 40 | /// Get as a string slice 41 | pub fn as_str(&self) -> &str { 42 | &self.name 43 | } 44 | } 45 | 46 | impl fmt::Display for Name { 47 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 48 | write!(f, "'{}'", self.name) 49 | } 50 | } 51 | 52 | impl Queryable for Name { 53 | fn query<'b, T: VariantValue, Registry: FunctionRegistry>( 54 | &self, 55 | current: &'b T, 56 | _root: &'b T, 57 | _registry: &Registry, 58 | ) -> Vec<&'b T> { 59 | let name = self.name.as_str(); 60 | current 61 | .as_object() 62 | .and_then(|o| o.get(name)) 63 | .map(|v| vec![v]) 64 | .unwrap_or_default() 65 | } 66 | 67 | fn query_located<'b, T: VariantValue, Registry: FunctionRegistry>( 68 | &self, 69 | current: &'b T, 70 | _root: &'b T, 71 | _registry: &Registry, 72 | mut parent: NormalizedPath<'b>, 73 | ) -> Vec> { 74 | let name = self.name.as_str(); 75 | current 76 | .as_object() 77 | .and_then(|o| o.get_key_value(name)) 78 | .map(|(k, v)| { 79 | parent.push(k); 80 | vec![LocatedNode::new(parent, v)] 81 | }) 82 | .unwrap_or_default() 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /spath/src/spec/selector/slice.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 tison 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Slice selectors for selecting array slices in SPath. 16 | 17 | use std::cmp::max; 18 | use std::cmp::min; 19 | use std::cmp::Ordering; 20 | 21 | use num_traits::ToPrimitive; 22 | 23 | use crate::spec::function::FunctionRegistry; 24 | use crate::spec::query::Queryable; 25 | use crate::ConcreteVariantArray; 26 | use crate::LocatedNode; 27 | use crate::NormalizedPath; 28 | use crate::VariantValue; 29 | 30 | /// §2.3.4 Array Slice Selector. 31 | /// 32 | /// Default Array Slice start and end Values: 33 | /// 34 | /// | Condition | start | end | 35 | /// |-----------|-----------|---------| 36 | /// | step >= 0 | 0 | len | 37 | /// | step < 0 | len - 1 | -len - 1| 38 | #[derive(Debug, PartialEq, Eq, Default, Clone, Copy)] 39 | pub struct Slice { 40 | /// The start of the slice, inclusive. 41 | /// 42 | /// This can be negative to start the slice from a position relative to the end of the array 43 | /// being sliced. 44 | start: Option, 45 | /// The end of the slice, exclusive. 46 | /// 47 | /// This can be negative to end the slice at a position relative to the end of the array being 48 | /// sliced. 49 | end: Option, 50 | /// The step slice for the slice. Default to 1. 51 | /// 52 | /// This can be negative to step in reverse order. 53 | step: Option, 54 | } 55 | 56 | impl std::fmt::Display for Slice { 57 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 58 | if let Some(start) = self.start { 59 | write!(f, "{start}")?; 60 | } 61 | write!(f, ":")?; 62 | if let Some(end) = self.end { 63 | write!(f, "{end}")?; 64 | } 65 | write!(f, ":")?; 66 | if let Some(step) = self.step { 67 | write!(f, "{step}")?; 68 | } 69 | Ok(()) 70 | } 71 | } 72 | 73 | // §2.3.4.2.2. (Array Slice Selector) Normative Semantics 74 | fn bounds(start: i64, end: i64, step: i64, len: i64) -> (i64, i64) { 75 | fn normalize(i: i64, len: i64) -> i64 { 76 | if i < 0 { 77 | len + i 78 | } else { 79 | i 80 | } 81 | } 82 | 83 | let start = normalize(start, len); 84 | let end = normalize(end, len); 85 | 86 | if step >= 0 { 87 | let lower = min(max(start, 0), len); 88 | let upper = min(max(end, 0), len); 89 | (lower, upper) 90 | } else { 91 | let upper = min(max(start, -1), len - 1); 92 | let lower = min(max(end, -1), len - 1); 93 | (lower, upper) 94 | } 95 | } 96 | 97 | impl Slice { 98 | /// Create a new slice selector. 99 | pub fn new(start: Option, end: Option, step: Option) -> Self { 100 | Self { start, end, step } 101 | } 102 | 103 | fn select<'b, T, N, F>(&self, current: &'b T, make_node: F) -> Vec 104 | where 105 | T: VariantValue, 106 | N: 'b, 107 | F: Fn(usize, &'b T) -> N, 108 | { 109 | let vec = match current.as_array() { 110 | Some(vec) => vec, 111 | None => return vec![], 112 | }; 113 | 114 | let (start, end, step) = (self.start, self.end, self.step.unwrap_or(1)); 115 | if step == 0 { 116 | // §2.3.4.2.2. Normative Semantics 117 | // When step = 0, no elements are selected, and the result array is empty. 118 | return vec![]; 119 | } 120 | 121 | let len = vec.len().to_i64().unwrap_or(i64::MAX); 122 | let (start, end) = if step >= 0 { 123 | match (start, end) { 124 | (Some(start), Some(end)) => (start, end), 125 | (Some(start), None) => (start, len), 126 | (None, Some(end)) => (0, end), 127 | (None, None) => (0, len), 128 | } 129 | } else { 130 | match (start, end) { 131 | (Some(start), Some(end)) => (start, end), 132 | (Some(start), None) => (start, -len - 1), 133 | (None, Some(end)) => (len - 1, end), 134 | (None, None) => (len - 1, -len - 1), 135 | } 136 | }; 137 | 138 | let (lower, upper) = bounds(start, end, step, len); 139 | let mut selected = vec![]; 140 | match step.cmp(&0) { 141 | Ordering::Greater => { 142 | // step > 0 143 | let mut i = lower; 144 | while i < upper { 145 | let node = vec.get(i as usize).unwrap(); 146 | selected.push(make_node(i as usize, node)); 147 | i += step; 148 | } 149 | } 150 | Ordering::Less => { 151 | // step < 0 152 | let mut i = upper; 153 | while lower < i { 154 | let node = vec.get(i as usize).unwrap(); 155 | selected.push(make_node(i as usize, node)); 156 | i += step; 157 | } 158 | } 159 | Ordering::Equal => unreachable!("step is guaranteed not zero here"), 160 | } 161 | selected 162 | } 163 | } 164 | 165 | impl Queryable for Slice { 166 | fn query<'b, T: VariantValue, Registry: FunctionRegistry>( 167 | &self, 168 | current: &'b T, 169 | _root: &'b T, 170 | _registry: &Registry, 171 | ) -> Vec<&'b T> { 172 | self.select(current, |_, node| node) 173 | } 174 | 175 | fn query_located<'b, T: VariantValue, Registry: FunctionRegistry>( 176 | &self, 177 | current: &'b T, 178 | _root: &'b T, 179 | _registry: &Registry, 180 | parent: NormalizedPath<'b>, 181 | ) -> Vec> { 182 | self.select(current, |i, node| { 183 | LocatedNode::new(parent.clone_and_push(i), node) 184 | }) 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /spath/src/toml.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 tison 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use num_cmp::NumCmp; 16 | use toml::Table; 17 | use toml::Value; 18 | 19 | use crate::spec::function; 20 | use crate::value::ConcreteVariantArray; 21 | use crate::value::ConcreteVariantObject; 22 | use crate::value::VariantValue; 23 | use crate::FromLiteral; 24 | use crate::Literal; 25 | 26 | pub type BuiltinFunctionRegistry = function::BuiltinFunctionRegistry; 27 | 28 | impl FromLiteral for Value { 29 | fn from_literal(literal: Literal) -> Option { 30 | match literal { 31 | Literal::Int(v) => Some(Value::Integer(v)), 32 | Literal::Float(v) => Some(Value::Float(v)), 33 | Literal::String(v) => Some(Value::String(v)), 34 | Literal::Bool(v) => Some(Value::Boolean(v)), 35 | Literal::Null => None, 36 | } 37 | } 38 | } 39 | 40 | impl VariantValue for Value { 41 | type VariantArray = Vec; 42 | type VariantObject = Table; 43 | 44 | fn is_null(&self) -> bool { 45 | // toml 1.0 does not have null 46 | // 47 | // @see https://github.com/toml-lang/toml/issues/975 48 | // @see https://github.com/toml-lang/toml.io/issues/70 49 | // @see https://github.com/toml-lang/toml/issues/30 50 | false 51 | } 52 | 53 | fn is_boolean(&self) -> bool { 54 | self.is_bool() 55 | } 56 | 57 | fn is_string(&self) -> bool { 58 | self.is_str() 59 | } 60 | 61 | fn is_array(&self) -> bool { 62 | self.is_array() 63 | } 64 | 65 | fn is_object(&self) -> bool { 66 | self.is_table() 67 | } 68 | 69 | fn as_bool(&self) -> Option { 70 | self.as_bool() 71 | } 72 | 73 | fn as_str(&self) -> Option<&str> { 74 | self.as_str() 75 | } 76 | 77 | fn as_array(&self) -> Option<&Self::VariantArray> { 78 | self.as_array() 79 | } 80 | 81 | fn as_object(&self) -> Option<&Self::VariantObject> { 82 | self.as_table() 83 | } 84 | 85 | fn is_less_than(&self, other: &Self) -> bool { 86 | match (self, other) { 87 | (Value::Integer(l), Value::Float(r)) => NumCmp::num_lt(*l, *r), 88 | (Value::Float(l), Value::Integer(r)) => NumCmp::num_lt(*l, *r), 89 | (Value::String(l), Value::String(r)) => l < r, 90 | _ => false, 91 | } 92 | } 93 | 94 | fn is_equal_to(&self, other: &Self) -> bool { 95 | match (self, other) { 96 | (Value::Integer(l), Value::Float(r)) => NumCmp::num_eq(*l, *r), 97 | (Value::Float(l), Value::Integer(r)) => NumCmp::num_eq(*l, *r), 98 | _ => self == other, 99 | } 100 | } 101 | } 102 | 103 | impl ConcreteVariantArray for Vec { 104 | type Value = Value; 105 | 106 | fn is_empty(&self) -> bool { 107 | self.is_empty() 108 | } 109 | 110 | fn len(&self) -> usize { 111 | self.len() 112 | } 113 | 114 | fn get(&self, index: usize) -> Option<&Self::Value> { 115 | (**self).get(index) 116 | } 117 | 118 | fn iter(&self) -> impl Iterator { 119 | (**self).iter() 120 | } 121 | } 122 | 123 | impl ConcreteVariantObject for Table { 124 | type Value = Value; 125 | 126 | fn is_empty(&self) -> bool { 127 | self.is_empty() 128 | } 129 | 130 | fn len(&self) -> usize { 131 | self.len() 132 | } 133 | 134 | fn get(&self, key: &str) -> Option<&Self::Value> { 135 | self.get(key) 136 | } 137 | 138 | fn get_key_value(&self, key: &str) -> Option<(&String, &Self::Value)> { 139 | self.get_key_value(key) 140 | } 141 | 142 | fn iter(&self) -> impl Iterator { 143 | self.iter() 144 | } 145 | 146 | fn values(&self) -> impl Iterator { 147 | self.values() 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /spath/src/value.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 tison 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Public structs and traits for variant (semi-structured) data values. 16 | 17 | use std::fmt; 18 | 19 | /// A literal variant value that can be represented in an SPath query. 20 | #[derive(Debug, Clone)] 21 | pub enum Literal { 22 | /// 64-bit integer. 23 | Int(i64), 24 | /// 64-bit floating point number. 25 | Float(f64), 26 | /// UTF-8 string. 27 | String(String), 28 | /// `true` or `false`. 29 | Bool(bool), 30 | /// `null`. 31 | Null, 32 | } 33 | 34 | impl fmt::Display for Literal { 35 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 36 | match self { 37 | Literal::Int(n) => write!(f, "{n}"), 38 | Literal::Float(n) => write!(f, "{n:?}"), 39 | Literal::String(s) => write!(f, "'{s}'"), 40 | Literal::Bool(b) => write!(f, "{b}"), 41 | Literal::Null => write!(f, "null"), 42 | } 43 | } 44 | } 45 | 46 | /// A trait for converting a literal to a variant value. 47 | pub trait FromLiteral { 48 | fn from_literal(literal: Literal) -> Option 49 | where 50 | Self: Sized; 51 | } 52 | 53 | /// A trait for any variant value. 54 | pub trait VariantValue: FromLiteral { 55 | /// The type of the array variant. 56 | type VariantArray: ConcreteVariantArray; 57 | /// The type of the object variant. 58 | type VariantObject: ConcreteVariantObject; 59 | /// Whether the value is a null. 60 | fn is_null(&self) -> bool; 61 | /// Whether the value is a boolean. 62 | fn is_boolean(&self) -> bool; 63 | /// Whether the value is a number. 64 | fn is_string(&self) -> bool; 65 | /// Whether the value is an array. 66 | fn is_array(&self) -> bool; 67 | /// Whether the value is an object. 68 | fn is_object(&self) -> bool; 69 | /// Convert the value to a bool; [`None`] if the value is not an array. 70 | fn as_bool(&self) -> Option; 71 | /// Convert the value to a str; [`None`] if the value is not a string. 72 | fn as_str(&self) -> Option<&str>; 73 | /// Convert the value to an array; [`None`] if the value is not an array. 74 | fn as_array(&self) -> Option<&Self::VariantArray>; 75 | /// Convert the value to an object; [`None`] if the value is not an object. 76 | fn as_object(&self) -> Option<&Self::VariantObject>; 77 | 78 | // §2.3.5.2.2 Comparisons 79 | /// Whether self is less than another value. 80 | fn is_less_than(&self, other: &Self) -> bool; 81 | /// Whether self is equal to another value. 82 | fn is_equal_to(&self, other: &Self) -> bool; 83 | } 84 | 85 | /// A trait for the concrete variant array type associated with a variant value. 86 | pub trait ConcreteVariantArray { 87 | /// The type of the value in the array. 88 | type Value: VariantValue; 89 | /// Whether the array is empty. 90 | fn is_empty(&self) -> bool; 91 | /// The length of the array. 92 | fn len(&self) -> usize; 93 | /// Get the value at the given index; [`None`] if the index is out of bounds. 94 | fn get(&self, index: usize) -> Option<&Self::Value>; 95 | /// An iterator over the values in the array. 96 | fn iter(&self) -> impl Iterator; 97 | } 98 | 99 | /// A trait for the concrete variant object type associated with a variant value. 100 | pub trait ConcreteVariantObject { 101 | /// The type of the value in the object. 102 | type Value: VariantValue; 103 | /// Whether the object is empty. 104 | fn is_empty(&self) -> bool; 105 | /// The length of the object, i.e., the number of key-value pairs. 106 | fn len(&self) -> usize; 107 | /// Get the value for the given key; [`None`] if the key is not present. 108 | fn get(&self, key: &str) -> Option<&Self::Value>; 109 | /// Get the key-value pair for the given key; [`None`] if the key is not present. 110 | fn get_key_value(&self, key: &str) -> Option<(&String, &Self::Value)>; 111 | /// An iterator over the key-value pairs in the object. 112 | fn iter(&self) -> impl Iterator; 113 | /// An iterator over the values in the object. 114 | fn values(&self) -> impl Iterator; 115 | } 116 | -------------------------------------------------------------------------------- /spath/testdata/learn-toml-in-y-minutes.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 tison 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Comments in TOML look like this. 16 | 17 | ################ 18 | # SCALAR TYPES # 19 | ################ 20 | 21 | # Our root object (which continues for the entire document) will be a map, 22 | # which is equivalent to a dictionary, hash or object in other languages. 23 | 24 | # The key, equals sign, and value must be on the same line 25 | # (though some values can be broken over multiple lines). 26 | boolean = true 27 | dateTime = 1979-05-27T07:32:00-08:00 28 | float = 3.14 29 | key = "value" 30 | "key can be quoted" = true # Both " and ' are fine 31 | number = 42 32 | scientificNotation = 1e+12 33 | string = "hello" 34 | "unquoted key may contain" = "letters, numbers, underscores, and dashes" 35 | 36 | ########## 37 | # String # 38 | ########## 39 | 40 | # All strings must contain only valid UTF-8 characters. 41 | # We can escape characters and some of them have a compact escape sequence. 42 | # For example, \t add a tabulation. Refers to the spec to get all of them. 43 | basicString = "are surrounded by quotation marks. \"I'm quotable\". Name\tJos" 44 | 45 | multiLineString = """ 46 | are surrounded by three quotation marks 47 | on each side and allow newlines.""" 48 | 49 | literalString = 'are surrounded by single quotes. Escaping are not allowed.' 50 | 51 | multiLineLiteralString = ''' 52 | are surrounded by three single quotes on each side 53 | and allow newlines. Still no escaping. 54 | The first newline is trimmed in raw strings. 55 | All other whitespace 56 | is preserved. #! are preserved? 57 | ''' 58 | 59 | # For binary data it is recommended that you use Base64, another ASCII or UTF8 60 | # encoding. The handling of that encoding will be application specific. 61 | 62 | ########### 63 | # Integer # 64 | ########### 65 | 66 | ## Integers can start with a +, a - or nothing. 67 | ## Leading zeros are not allowed. 68 | ## Hex, octal, and binary forms are allowed. 69 | ## Values that cannot be expressed as a series of digits are not allowed. 70 | int1 = +42 71 | int2 = 0 72 | int3 = -21 73 | int4 = 0xdeadbeef 74 | int5 = 0o755 75 | int6 = 0b11011100 76 | integerRange = 64 77 | 78 | ######### 79 | # Float # 80 | ######### 81 | 82 | # Floats are an integer followed by a fractional and/or an exponent part. 83 | flt1 = 3.1415 84 | flt2 = -5e6 85 | flt3 = 6.626E-34 86 | 87 | ########### 88 | # Boolean # 89 | ########### 90 | 91 | bool1 = true 92 | bool2 = false 93 | boolMustBeLowercase = true 94 | 95 | ############ 96 | # Datetime # 97 | ############ 98 | 99 | date1 = 1979-05-27T07:32:00Z # UTC time, following RFC 3339/ISO 8601 spec 100 | date2 = 1979-05-26T15:32:00+08:00 # with RFC 3339/ISO 8601 offset 101 | date3 = 1979-05-27T07:32:00 # without offset 102 | date4 = 1979-05-27 # without offset or time 103 | 104 | #################### 105 | # COLLECTION TYPES # 106 | #################### 107 | 108 | ######### 109 | # Array # 110 | ######### 111 | 112 | array1 = [1, 2, 3] 113 | array2 = ["Commas", "are", "delimiters"] 114 | array3 = ["Don't mix", "different", "types"] 115 | array4 = [[1.2, 2.4], ["all", 'strings', """are the same""", '''type''']] 116 | array5 = ["Whitespace", "is", "ignored"] 117 | 118 | ######### 119 | # Table # 120 | ######### 121 | 122 | # Tables (or hash tables or dictionaries) are collections of key/value 123 | # pairs. They appear in square brackets on a line by themselves. 124 | # Empty tables are allowed and simply have no key/value pairs within them. 125 | [table] 126 | 127 | # Under that, and until the next table or EOF are the key/values of that table. 128 | # Key/value pairs within tables are not guaranteed to be in any specific order. 129 | [table-1] 130 | key1 = "some string" 131 | key2 = 123 132 | 133 | [table-2] 134 | key1 = "another string" 135 | key2 = 456 136 | 137 | # Dots are prohibited in bare keys because dots are used to signify nested tables. 138 | # Naming rules for each dot separated part are the same as for keys. 139 | [dog."tater.man"] 140 | type = "pug" 141 | 142 | # In JSON land, that would give you the following structure: 143 | # { "dog": { "tater.man": { "type": "pug" } } } 144 | 145 | # Whitespace around dot-separated parts is ignored, however, best practice is to 146 | # not use any extraneous whitespace. 147 | [a.b.c] # this is best practice 148 | [d.e.f] # same as [d.e.f] 149 | [j."ʞ".'l'] # same as [j."ʞ".'l'] 150 | 151 | # You don't need to specify all the super-tables if you don't want to. TOML knows 152 | # how to do it for you. 153 | # [x] you 154 | # [x.y] don't 155 | # [x.y.z] need these 156 | [x.y.z.w] # for this to work 157 | 158 | ################### 159 | # Array of Tables # 160 | ################### 161 | 162 | # An array of tables can be expressed by using a table name in double brackets. 163 | # Each table with the same double bracketed name will be an item in the array. 164 | # The tables are inserted in the order encountered. 165 | 166 | [[products]] 167 | emptyTableAreAllowed = true 168 | name = "array of table" 169 | sku = 738594937 170 | 171 | [[products]] 172 | 173 | [[products]] 174 | color = "gray" 175 | name = "Nail" 176 | sku = 284758393 177 | -------------------------------------------------------------------------------- /spath/testdata/rfc-9535-example-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "store": { 3 | "book": [ 4 | { 5 | "category": "reference", 6 | "author": "Nigel Rees", 7 | "title": "Sayings of the Century", 8 | "price": 8.95 9 | }, 10 | { 11 | "category": "fiction", 12 | "author": "Evelyn Waugh", 13 | "title": "Sword of Honour", 14 | "price": 12.99 15 | }, 16 | { 17 | "category": "fiction", 18 | "author": "Herman Melville", 19 | "title": "Moby Dick", 20 | "isbn": "0-553-21311-3", 21 | "price": 8.99 22 | }, 23 | { 24 | "category": "fiction", 25 | "author": "J. R. R. Tolkien", 26 | "title": "The Lord of the Rings", 27 | "isbn": "0-395-19395-8", 28 | "price": 22.99 29 | } 30 | ], 31 | "bicycle": { 32 | "color": "red", 33 | "price": 399 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /spath/testdata/rfc-9535-example-10.json: -------------------------------------------------------------------------------- 1 | {"a": null, "b": [null], "c": [{}], "null": 1} 2 | -------------------------------------------------------------------------------- /spath/testdata/rfc-9535-example-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "o": {"j j": {"k.k": 3}}, 3 | "'": {"@": 2} 4 | } 5 | -------------------------------------------------------------------------------- /spath/testdata/rfc-9535-example-3.json: -------------------------------------------------------------------------------- 1 | { 2 | "o": {"j": 1, "k": 2}, 3 | "a": [5, 3] 4 | } 5 | -------------------------------------------------------------------------------- /spath/testdata/rfc-9535-example-4.json: -------------------------------------------------------------------------------- 1 | ["a","b"] 2 | -------------------------------------------------------------------------------- /spath/testdata/rfc-9535-example-5.json: -------------------------------------------------------------------------------- 1 | ["a", "b", "c", "d", "e", "f", "g"] 2 | -------------------------------------------------------------------------------- /spath/testdata/rfc-9535-example-6.json: -------------------------------------------------------------------------------- 1 | { 2 | "obj": {"x": "y"}, 3 | "arr": [2, 3] 4 | } 5 | -------------------------------------------------------------------------------- /spath/testdata/rfc-9535-example-7.json: -------------------------------------------------------------------------------- 1 | { 2 | "a": [3, 5, 1, 2, 4, 6, 3 | {"b": "j"}, 4 | {"b": "k"}, 5 | {"b": {}}, 6 | {"b": "kilo"} 7 | ], 8 | "o": {"p": 1, "q": 2, "r": 3, "s": 5, "t": {"u": 6}}, 9 | "e": "f" 10 | } 11 | -------------------------------------------------------------------------------- /spath/testdata/rfc-9535-example-8.json: -------------------------------------------------------------------------------- 1 | ["a", "b", "c", "d", "e", "f", "g"] 2 | -------------------------------------------------------------------------------- /spath/testdata/rfc-9535-example-9.json: -------------------------------------------------------------------------------- 1 | { 2 | "o": {"j": 1, "k": 2}, 3 | "a": [5, 3, [{"j": 4}, {"k": 6}]] 4 | } 5 | -------------------------------------------------------------------------------- /spath/tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 tison 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | pub fn manifest_dir() -> std::path::PathBuf { 16 | let dir = env!("CARGO_MANIFEST_DIR"); 17 | std::path::PathBuf::from(dir).canonicalize().unwrap() 18 | } 19 | -------------------------------------------------------------------------------- /spath/tests/spec.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 tison 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #![cfg(feature = "json")] 16 | 17 | //! Spec tests based on RFC 9535. 18 | 19 | mod common; 20 | 21 | use common::manifest_dir; 22 | use googletest::assert_that; 23 | use googletest::matchers::container_eq; 24 | use googletest::matchers::none; 25 | use googletest::prelude::eq; 26 | use googletest::prelude::some; 27 | use insta::assert_compact_json_snapshot; 28 | use serde_json::json; 29 | use spath::NodeList; 30 | use spath::SPath; 31 | 32 | fn json_testdata(filename: &str) -> serde_json::Value { 33 | let path = manifest_dir().join("testdata").join(filename); 34 | let content = std::fs::read_to_string(path).unwrap(); 35 | serde_json::from_str(&content).unwrap() 36 | } 37 | 38 | fn eval_spath<'a>( 39 | spath: &str, 40 | value: &'a serde_json::Value, 41 | ) -> Result, spath::ParseError> { 42 | let registry = spath::json::BuiltinFunctionRegistry::default(); 43 | let spath = SPath::parse_with_registry(spath, registry)?; 44 | Ok(spath.query(value)) 45 | } 46 | 47 | #[test] 48 | fn test_root_identical() { 49 | let value = json_testdata("rfc-9535-example-1.json"); 50 | let result = eval_spath("$", &value).unwrap(); 51 | let result = result.exactly_one().unwrap(); 52 | assert_that!(result, eq(&value)); 53 | } 54 | 55 | #[test] 56 | fn test_basic_name_selector() { 57 | let value = json_testdata("rfc-9535-example-1.json"); 58 | let result = eval_spath(r#"$["store"]['bicycle']"#, &value).unwrap(); 59 | let result = result.exactly_one().unwrap(); 60 | assert_compact_json_snapshot!(result, @r#"{"color": "red", "price": 399}"#); 61 | let result = eval_spath(r#"$.store.bicycle.color"#, &value).unwrap(); 62 | let result = result.exactly_one().unwrap(); 63 | assert_compact_json_snapshot!(result, @r#""red""#); 64 | let result = eval_spath(r#"$.store.book.*"#, &value).unwrap(); 65 | let result = result.all(); 66 | assert_compact_json_snapshot!(result, @r#" 67 | [ 68 | { 69 | "author": "Nigel Rees", 70 | "category": "reference", 71 | "price": 8.95, 72 | "title": "Sayings of the Century" 73 | }, 74 | { 75 | "author": "Evelyn Waugh", 76 | "category": "fiction", 77 | "price": 12.99, 78 | "title": "Sword of Honour" 79 | }, 80 | { 81 | "author": "Herman Melville", 82 | "category": "fiction", 83 | "isbn": "0-553-21311-3", 84 | "price": 8.99, 85 | "title": "Moby Dick" 86 | }, 87 | { 88 | "author": "J. R. R. Tolkien", 89 | "category": "fiction", 90 | "isbn": "0-395-19395-8", 91 | "price": 22.99, 92 | "title": "The Lord of the Rings" 93 | } 94 | ] 95 | "#); 96 | 97 | // §2.3.1.3 (Example) Table 5: Name Selector Examples 98 | let value = json_testdata("rfc-9535-example-2.json"); 99 | let result = eval_spath(r#"$.o['j j']"#, &value).unwrap(); 100 | let result = result.exactly_one().unwrap(); 101 | assert_compact_json_snapshot!(result, @r#"{"k.k": 3}"#); 102 | let result = eval_spath(r#"$.o['j j']['k.k'] "#, &value).unwrap(); 103 | let result = result.exactly_one().unwrap(); 104 | assert_compact_json_snapshot!(result, @"3"); 105 | let result = eval_spath(r#"$.o["j j"]["k.k"]"#, &value).unwrap(); 106 | let result = result.exactly_one().unwrap(); 107 | assert_compact_json_snapshot!(result, @"3"); 108 | let result = eval_spath(r#"$["'"]["@"]"#, &value).unwrap(); 109 | let result = result.exactly_one().unwrap(); 110 | assert_compact_json_snapshot!(result, @"2"); 111 | } 112 | 113 | #[test] 114 | fn test_basic_wildcard_selector() { 115 | // §2.3.2.3 (Example) Table 6: Wildcard Selector Examples 116 | let value = json_testdata("rfc-9535-example-3.json"); 117 | let result = eval_spath(r#"$[*]"#, &value).unwrap(); 118 | let result = result.all(); 119 | assert_compact_json_snapshot!(result, @r#"[[5, 3], {"j": 1, "k": 2}]"#); 120 | let result = eval_spath(r#"$.o[*]"#, &value).unwrap(); 121 | let result = result.all(); 122 | assert_compact_json_snapshot!(result, @"[1, 2]"); 123 | let result = eval_spath(r#"$.o[*, *]"#, &value).unwrap(); 124 | let result = result.all(); 125 | assert_compact_json_snapshot!(result, @"[1, 2, 1, 2]"); 126 | let result = eval_spath(r#"$.a[*]"#, &value).unwrap(); 127 | let result = result.all(); 128 | assert_compact_json_snapshot!(result, @"[5, 3]"); 129 | } 130 | 131 | #[test] 132 | fn test_basic_index_slice_selector() { 133 | // §2.3.3.3 (Example) Table 7: Index Selector Examples 134 | let value = json_testdata("rfc-9535-example-4.json"); 135 | let result = eval_spath(r#"$[1]"#, &value).unwrap(); 136 | let result = result.exactly_one().unwrap(); 137 | assert_compact_json_snapshot!(result, @r#""b""#); 138 | let result = eval_spath(r#"$[0]"#, &value).unwrap(); 139 | let result = result.exactly_one().unwrap(); 140 | assert_compact_json_snapshot!(result, @r#""a""#); 141 | } 142 | 143 | #[test] 144 | fn test_basic_array_slice_selector() { 145 | // §2.3.4.3 (Example) Table 9: Array Slice Selector Examples 146 | let value = json_testdata("rfc-9535-example-5.json"); 147 | let result = eval_spath(r#"$[1:3]"#, &value).unwrap(); 148 | let result = result.all(); 149 | assert_compact_json_snapshot!(result, @r#"["b", "c"]"#); 150 | let result = eval_spath(r#"$[5:]"#, &value).unwrap(); 151 | let result = result.all(); 152 | assert_compact_json_snapshot!(result, @r#"["f", "g"]"#); 153 | let result = eval_spath(r#"$[1:5:2]"#, &value).unwrap(); 154 | let result = result.all(); 155 | assert_compact_json_snapshot!(result, @r#"["b", "d"]"#); 156 | let result = eval_spath(r#"$[5:1:-2]"#, &value).unwrap(); 157 | let result = result.all(); 158 | assert_compact_json_snapshot!(result, @r#"["f", "d"]"#); 159 | let result = eval_spath(r#"$[::-1]"#, &value).unwrap(); 160 | let result = result.all(); 161 | assert_compact_json_snapshot!(result, @r#"["g", "f", "e", "d", "c", "b", "a"]"#); 162 | } 163 | 164 | #[test] 165 | fn test_basic_child_and_descendant_segment() { 166 | // §2.5.1.3 (Example) Table 15: Child Segment Examples 167 | let value = json_testdata("rfc-9535-example-8.json"); 168 | let result = eval_spath(r#"$[0, 3]"#, &value).unwrap(); 169 | let result = result.all(); 170 | assert_compact_json_snapshot!(result, @r#"["a", "d"]"#); 171 | let result = eval_spath(r#"$[0:2, 5]"#, &value).unwrap(); 172 | let result = result.all(); 173 | assert_compact_json_snapshot!(result, @r#"["a", "b", "f"]"#); 174 | let result = eval_spath(r#"$[0,0]"#, &value).unwrap(); 175 | let result = result.all(); 176 | assert_compact_json_snapshot!(result, @r#"["a", "a"]"#); 177 | 178 | // §2.5.2.3 (Example) Table 16: Descendant Segment Examples 179 | let value = json_testdata("rfc-9535-example-9.json"); 180 | let result = eval_spath(r#"$..j"#, &value).unwrap(); 181 | let result = result.all(); 182 | assert_compact_json_snapshot!(result, @"[4, 1]"); 183 | let result = eval_spath(r#"$..[0]"#, &value).unwrap(); 184 | let result = result.all(); 185 | assert_compact_json_snapshot!(result, @r#"[5, {"j": 4}]"#); 186 | let result = eval_spath(r#"$..*"#, &value).unwrap(); 187 | let result = result.all(); 188 | assert_compact_json_snapshot!(result, @r#"[[5, 3, [{"j": 4}, {"k": 6}]], {"j": 1, "k": 2}, 5, 3, [{"j": 4}, {"k": 6}], {"j": 4}, {"k": 6}, 4, 6, 1, 2]"#); 189 | let result = eval_spath(r#"$..[*]"#, &value).unwrap(); 190 | let result = result.all(); 191 | assert_compact_json_snapshot!(result, @r#"[[5, 3, [{"j": 4}, {"k": 6}]], {"j": 1, "k": 2}, 5, 3, [{"j": 4}, {"k": 6}], {"j": 4}, {"k": 6}, 4, 6, 1, 2]"#); 192 | let result = eval_spath(r#"$..o"#, &value).unwrap(); 193 | let result = result.all(); 194 | assert_compact_json_snapshot!(result, @r#"[{"j": 1, "k": 2}]"#); 195 | let result = eval_spath(r#"$.o..[*, *]"#, &value).unwrap(); 196 | let result = result.all(); 197 | assert_compact_json_snapshot!(result, @"[1, 2, 1, 2]"); 198 | let result = eval_spath(r#"$.a..[0, 1]"#, &value).unwrap(); 199 | let result = result.all(); 200 | assert_compact_json_snapshot!(result, @r#"[5, 3, {"j": 4}, {"k": 6}]"#); 201 | } 202 | 203 | #[test] 204 | fn test_basic_null_semantic() { 205 | // §2.6 Semantics of null 206 | // 207 | // JSON null is treated the same as any other JSON value, i.e., 208 | // it is not taken to mean "undefined" or "missing". 209 | 210 | // §2.6.1 (Example) Table 17: Examples Involving (or Not Involving) null 211 | let value = json_testdata("rfc-9535-example-10.json"); 212 | let null = serde_json::Value::Null; 213 | let array_of_null = [null.clone()]; 214 | let value_of_ident_null = serde_json::Value::Number(1i64.into()); 215 | 216 | let result = eval_spath(r#"$.a"#, &value).unwrap(); 217 | assert_that!(result.at_most_one().unwrap(), some(eq(&null))); 218 | let result = eval_spath(r#"$.a[0]"#, &value).unwrap(); 219 | assert_that!(result.at_most_one().unwrap(), none()); 220 | let result = eval_spath(r#"$.a.d"#, &value).unwrap(); 221 | assert_that!(result.at_most_one().unwrap(), none()); 222 | let result = eval_spath(r#"$.b[0]"#, &value).unwrap(); 223 | assert_that!(result.at_most_one().unwrap(), some(eq(&null))); 224 | let result = eval_spath(r#"$.b[*]"#, &value).unwrap(); 225 | assert_that!( 226 | result.all(), 227 | container_eq(array_of_null.iter().collect::>()) 228 | ); 229 | let result = eval_spath(r#"$.null"#, &value).unwrap(); 230 | assert_that!( 231 | result.at_most_one().unwrap(), 232 | some(eq(&value_of_ident_null)) 233 | ); 234 | } 235 | 236 | #[test] 237 | fn test_filters() { 238 | // §2.3.5.3 Table 12: Filter Selector Examples 239 | let value = json! {{ 240 | "a": [3, 5, 1, 2, 4, 6, 241 | {"b": "j"}, 242 | {"b": "k"}, 243 | {"b": {}}, 244 | {"b": "kilo"} 245 | ], 246 | "o": {"p": 1, "q": 2, "r": 3, "s": 5, "t": {"u": 6}}, 247 | "e": "f" 248 | }}; 249 | 250 | let result = eval_spath(r#"$.a[?@.b == 'kilo']"#, &value).unwrap(); 251 | let result = result.exactly_one().unwrap(); 252 | assert_compact_json_snapshot!(result, @r#"{"b": "kilo"}"#); 253 | let result = eval_spath(r#"$.a[?(@.b == 'kilo')]"#, &value).unwrap(); 254 | let result = result.exactly_one().unwrap(); 255 | assert_compact_json_snapshot!(result, @r#"{"b": "kilo"}"#); 256 | let result = eval_spath(r#"$.a[?@>3.5]"#, &value).unwrap(); 257 | let result = result.all(); 258 | assert_compact_json_snapshot!(result, @r#"[5, 4, 6]"#); 259 | let result = eval_spath(r#"$.a[?@.b]"#, &value).unwrap(); 260 | let result = result.all(); 261 | assert_compact_json_snapshot!(result, @r#"[{"b": "j"}, {"b": "k"}, {"b": {}}, {"b": "kilo"}]"#); 262 | } 263 | #[test] 264 | fn test_filter_functions() { 265 | let values = json! {{ 266 | "a": [1, 2, 3, 4, 5], 267 | "b": 42, 268 | "c": [], 269 | "d": {"e": 1, "f": 2}, 270 | }}; 271 | 272 | let result = eval_spath(r#"$[?length(@) < 3]"#, &values).unwrap(); 273 | let result = result.all(); 274 | assert_compact_json_snapshot!(result, @r#"[[], {"e": 1, "f": 2}]"#); 275 | let result = eval_spath(r#"$[?count(@.*) > 1]"#, &values).unwrap(); 276 | let result = result.all(); 277 | assert_compact_json_snapshot!(result, @r#"[[1, 2, 3, 4, 5], {"e": 1, "f": 2}]"#); 278 | } 279 | 280 | #[test] 281 | #[cfg(feature = "regex")] 282 | fn test_regex_functions() { 283 | let values = json! {[ 284 | {"timezone": "UTC", "offset": 0}, 285 | {"timezone": "CET", "offset": 1}, 286 | {"timezone": "PST", "offset": -8}, 287 | {"timezone": "JST", "offset": 9}, 288 | ]}; 289 | let result = eval_spath(r#"$[?match(@.timezone, "...")].offset"#, &values).unwrap(); 290 | let result = result.all(); 291 | assert_compact_json_snapshot!(result, @"[0, 1, -8, 9]"); 292 | let result = eval_spath(r#"$[?match(@.timezone, "..")].offset"#, &values).unwrap(); 293 | let result = result.all(); 294 | assert_compact_json_snapshot!(result, @"[]"); 295 | let result = eval_spath(r#"$[?search(@.timezone, "ST")].offset"#, &values).unwrap(); 296 | let result = result.all(); 297 | assert_compact_json_snapshot!(result, @"[-8, 9]"); 298 | } 299 | -------------------------------------------------------------------------------- /spath/tests/toml.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 tison 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #![cfg(feature = "toml")] 16 | 17 | mod common; 18 | 19 | use common::manifest_dir; 20 | use googletest::assert_that; 21 | use googletest::matchers::eq; 22 | use insta::assert_compact_json_snapshot; 23 | use spath::NodeList; 24 | use spath::ParseError; 25 | use spath::SPath; 26 | use toml::Value; 27 | 28 | fn toml_testdata(filename: &str) -> Value { 29 | let path = manifest_dir().join("testdata").join(filename); 30 | let content = std::fs::read_to_string(path).unwrap(); 31 | toml::from_str(&content).unwrap() 32 | } 33 | 34 | fn eval_spath<'a>(spath: &str, value: &'a Value) -> Result, ParseError> { 35 | let registry = spath::toml::BuiltinFunctionRegistry::default(); 36 | let spath = SPath::parse_with_registry(spath, registry)?; 37 | Ok(spath.query(value)) 38 | } 39 | 40 | #[test] 41 | fn test_root_identical() { 42 | let value = toml_testdata("learn-toml-in-y-minutes.toml"); 43 | let result = eval_spath("$", &value).unwrap(); 44 | let result = result.exactly_one().unwrap(); 45 | assert_that!(result, eq(&value)); 46 | } 47 | 48 | #[test] 49 | fn test_casual() { 50 | let value = toml_testdata("learn-toml-in-y-minutes.toml"); 51 | let result = eval_spath(r#"$..["name"]"#, &value).unwrap(); 52 | let result = result.all(); 53 | assert_compact_json_snapshot!(result, @r#"["array of table", "Nail"]"#); 54 | let result = eval_spath(r#"$..[1]"#, &value).unwrap(); 55 | let result = result.all(); 56 | assert_compact_json_snapshot!(result, @r#"[2, "are", "different", ["all", "strings", "are the same", "type"], 2.4, "strings", "is", {}]"#); 57 | } 58 | -------------------------------------------------------------------------------- /taplo.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 tison 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | include = ["Cargo.toml", "**/*.toml"] 16 | 17 | [formatting] 18 | # Align consecutive entries vertically. 19 | align_entries = false 20 | # Append trailing commas for multi-line arrays. 21 | array_trailing_comma = true 22 | # Expand arrays to multiple lines that exceed the maximum column width. 23 | array_auto_expand = true 24 | # Collapse arrays that don't exceed the maximum column width and don't contain comments. 25 | array_auto_collapse = true 26 | # Omit white space padding from single-line arrays 27 | compact_arrays = true 28 | # Omit white space padding from the start and end of inline tables. 29 | compact_inline_tables = false 30 | # Maximum column width in characters, affects array expansion and collapse, this doesn't take whitespace into account. 31 | # Note that this is not set in stone, and works on a best-effort basis. 32 | column_width = 80 33 | # Indent based on tables and arrays of tables and their subtables, subtables out of order are not indented. 34 | indent_tables = false 35 | # The substring that is used for indentation, should be tabs or spaces (but technically can be anything). 36 | indent_string = ' ' 37 | # Add trailing newline at the end of the file if not present. 38 | trailing_newline = true 39 | # Alphabetically reorder keys that are not separated by empty lines. 40 | reorder_keys = true 41 | # Maximum amount of allowed consecutive blank lines. This does not affect the whitespace at the end of the document, as it is always stripped. 42 | allowed_blank_lines = 1 43 | # Use CRLF for line endings. 44 | crlf = false 45 | -------------------------------------------------------------------------------- /typos.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 tison 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | [default.extend-words] 16 | 17 | [files] 18 | extend-exclude = [] 19 | -------------------------------------------------------------------------------- /xtask/Cargo.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 tison 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | [package] 16 | name = "x" 17 | publish = false 18 | 19 | edition.workspace = true 20 | homepage.workspace = true 21 | license.workspace = true 22 | readme.workspace = true 23 | repository.workspace = true 24 | rust-version.workspace = true 25 | 26 | [package.metadata.release] 27 | release = false 28 | 29 | [dependencies] 30 | clap = { version = "4.5.23", features = ["derive"] } 31 | which = { version = "7.0.1" } 32 | 33 | [lints] 34 | workspace = true 35 | -------------------------------------------------------------------------------- /xtask/src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 tison 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::process::Command as StdCommand; 16 | 17 | use clap::Parser; 18 | use clap::Subcommand; 19 | 20 | #[derive(Parser)] 21 | struct Command { 22 | #[clap(subcommand)] 23 | sub: SubCommand, 24 | } 25 | 26 | impl Command { 27 | fn run(self) { 28 | match self.sub { 29 | SubCommand::Build(cmd) => cmd.run(), 30 | SubCommand::Lint(cmd) => cmd.run(), 31 | SubCommand::Test(cmd) => cmd.run(), 32 | } 33 | } 34 | } 35 | 36 | #[derive(Subcommand)] 37 | enum SubCommand { 38 | #[clap(about = "Compile workspace packages.")] 39 | Build(CommandBuild), 40 | #[clap(about = "Run format and clippy checks.")] 41 | Lint(CommandLint), 42 | #[clap(about = "Run unit tests.")] 43 | Test(CommandTest), 44 | } 45 | 46 | #[derive(Parser)] 47 | struct CommandBuild { 48 | #[arg(long, help = "Assert that `Cargo.lock` will remain unchanged.")] 49 | locked: bool, 50 | } 51 | 52 | impl CommandBuild { 53 | fn run(self) { 54 | run_command(make_build_cmd(self.locked)); 55 | } 56 | } 57 | 58 | #[derive(Parser)] 59 | struct CommandTest { 60 | #[arg(long, help = "Run tests serially and do not capture output.")] 61 | no_capture: bool, 62 | } 63 | 64 | impl CommandTest { 65 | fn run(self) { 66 | run_command(make_test_cmd(self.no_capture, true, true, &[])); 67 | run_command(make_test_cmd(self.no_capture, true, false, &[])); 68 | run_command(make_test_cmd(self.no_capture, true, false, &["json"])); 69 | run_command(make_test_cmd(self.no_capture, true, false, &["toml"])); 70 | } 71 | } 72 | 73 | #[derive(Parser)] 74 | #[clap(name = "lint")] 75 | struct CommandLint { 76 | #[arg(long, help = "Automatically apply lint suggestions.")] 77 | fix: bool, 78 | } 79 | 80 | impl CommandLint { 81 | fn run(self) { 82 | run_command(make_clippy_cmd(self.fix)); 83 | run_command(make_format_cmd(self.fix)); 84 | run_command(make_taplo_cmd(self.fix)); 85 | run_command(make_typos_cmd()); 86 | run_command(make_hawkeye_cmd(self.fix)); 87 | } 88 | } 89 | 90 | fn find_command(cmd: &str) -> StdCommand { 91 | match which::which(cmd) { 92 | Ok(exe) => { 93 | let mut cmd = StdCommand::new(exe); 94 | cmd.current_dir(env!("CARGO_WORKSPACE_DIR")); 95 | cmd 96 | } 97 | Err(err) => { 98 | panic!("{cmd} not found: {err}"); 99 | } 100 | } 101 | } 102 | 103 | fn ensure_installed(bin: &str, crate_name: &str) { 104 | if which::which(bin).is_err() { 105 | let mut cmd = find_command("cargo"); 106 | cmd.args(["install", crate_name]); 107 | run_command(cmd); 108 | } 109 | } 110 | 111 | fn run_command(mut cmd: StdCommand) { 112 | println!("{cmd:?}"); 113 | let status = cmd.status().expect("failed to execute process"); 114 | assert!(status.success(), "command failed: {status}"); 115 | } 116 | 117 | fn make_build_cmd(locked: bool) -> StdCommand { 118 | let mut cmd = find_command("cargo"); 119 | cmd.args([ 120 | "build", 121 | "--workspace", 122 | "--all-features", 123 | "--tests", 124 | "--examples", 125 | "--benches", 126 | "--bins", 127 | ]); 128 | if locked { 129 | cmd.arg("--locked"); 130 | } 131 | cmd 132 | } 133 | 134 | fn make_test_cmd( 135 | no_capture: bool, 136 | default_features: bool, 137 | all_features: bool, 138 | features: &[&str], 139 | ) -> StdCommand { 140 | let mut cmd = find_command("cargo"); 141 | cmd.args(["test", "--workspace"]); 142 | if all_features { 143 | cmd.arg("--all-features"); 144 | } else { 145 | if !default_features { 146 | cmd.arg("--no-default-features"); 147 | } 148 | if !features.is_empty() { 149 | cmd.args(["--features", features.join(",").as_str()]); 150 | } 151 | } 152 | if no_capture { 153 | cmd.args(["--", "--nocapture"]); 154 | } 155 | cmd 156 | } 157 | 158 | fn make_format_cmd(fix: bool) -> StdCommand { 159 | let mut cmd = find_command("cargo"); 160 | cmd.args(["fmt", "--all"]); 161 | if !fix { 162 | cmd.arg("--check"); 163 | } 164 | cmd 165 | } 166 | 167 | fn make_clippy_cmd(fix: bool) -> StdCommand { 168 | let mut cmd = find_command("cargo"); 169 | cmd.args([ 170 | "clippy", 171 | "--tests", 172 | "--all-features", 173 | "--all-targets", 174 | "--workspace", 175 | ]); 176 | if fix { 177 | cmd.args(["--allow-staged", "--allow-dirty", "--fix"]); 178 | } else { 179 | cmd.args(["--", "-D", "warnings"]); 180 | } 181 | cmd 182 | } 183 | 184 | fn make_hawkeye_cmd(fix: bool) -> StdCommand { 185 | ensure_installed("hawkeye", "hawkeye"); 186 | let mut cmd = find_command("hawkeye"); 187 | if fix { 188 | cmd.args(["format", "--fail-if-updated=false"]); 189 | } else { 190 | cmd.args(["check"]); 191 | } 192 | cmd 193 | } 194 | 195 | fn make_typos_cmd() -> StdCommand { 196 | ensure_installed("typos", "typos-cli"); 197 | find_command("typos") 198 | } 199 | 200 | fn make_taplo_cmd(fix: bool) -> StdCommand { 201 | ensure_installed("taplo", "taplo-cli"); 202 | let mut cmd = find_command("taplo"); 203 | if fix { 204 | cmd.args(["format"]); 205 | } else { 206 | cmd.args(["format", "--check"]); 207 | } 208 | cmd 209 | } 210 | 211 | fn main() { 212 | let cmd = Command::parse(); 213 | cmd.run() 214 | } 215 | --------------------------------------------------------------------------------