├── .cargo └── config.toml ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .rustfmt.toml ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── build-wheels.sh ├── py └── jsonlogic_rs │ ├── .gitignore │ └── __init__.py ├── pyproject.toml ├── scripts ├── build-all-linux-wheels.sh ├── newCargoVersion.sh ├── newNpmVersion.sh └── newPypiVersion.sh ├── setup.cfg ├── setup.py ├── src ├── bin.rs ├── error.rs ├── func.rs ├── js_op.rs ├── lib.rs ├── op │ ├── array.rs │ ├── data.rs │ ├── impure.rs │ ├── logic.rs │ ├── mod.rs │ ├── numeric.rs │ └── string.rs └── value.rs └── tests ├── README.md ├── data └── tests.json ├── test_lib.rs ├── test_py.py ├── test_py.rs ├── test_wasm.js └── test_wasm.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.x86_64-apple-darwin] 2 | rustflags = [ 3 | "-C", "link-arg=-undefined", 4 | "-C", "link-arg=dynamic_lookup", 5 | ] 6 | 7 | [target.aarch64-apple-darwin] 8 | rustflags = [ 9 | "-C", "link-arg=-undefined", 10 | "-C", "link-arg=dynamic_lookup", 11 | ] 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "Continuous Integration" 2 | on: 3 | push: 4 | branches: 5 | - "**" 6 | release: 7 | types: 8 | - published 9 | 10 | jobs: 11 | test: 12 | name: "Test" 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest, macos-latest] 16 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 17 | runs-on: "${{ matrix.os }}" 18 | steps: 19 | # Check out the code 20 | - uses: "actions/checkout@v4" 21 | 22 | # We need node for some integration tests 23 | - uses: "actions/setup-node@v4" 24 | 25 | # Install python 26 | - name: "Set up python" 27 | uses: "actions/setup-python@v5" 28 | with: 29 | python-version: "${{ matrix.python-version }}" 30 | 31 | - name: "Get Python Path" 32 | id: get-py-path 33 | shell: bash 34 | run: | 35 | echo "path=$(which python)" >> $GITHUB_OUTPUT 36 | 37 | # Set the current month and year (used for cache key) 38 | - name: "Get Date" 39 | id: get-date 40 | # Outputs e.g. "202007" 41 | # tbh I have yet to find the docs where this output format is 42 | # defined, but I copied this from the official cache action's README. 43 | run: | 44 | echo "date=$(/bin/date -u '+%Y%m')" >> $GITHUB_OUTPUT 45 | shell: bash 46 | 47 | # Generate the lockfile 48 | - name: "Generate Cargo Lockfile" 49 | run: "cargo generate-lockfile" 50 | 51 | # Cache build dependencies 52 | - name: "Cache Build Fragments" 53 | id: "cache-build-fragments" 54 | uses: "actions/cache@v4" 55 | with: 56 | path: | 57 | ~/.cargo/registry 58 | ~/.cargo/git 59 | target 60 | # Use the OS, the python version, and the hashed cargo lockfile as the 61 | # cache key. The Python version shouldn't be necessary, but I have 62 | # seen some weird failures in Windows CI where it gets the built 63 | # python targets confused. The Python version is included at the 64 | # end so it can be partially matched by cache keys in contexts 65 | # where we're not iterating over python envs. 66 | key: ${{ runner.os }}-${{ contains(runner.os, 'windows') && 'test-' || '' }}cargo-${{ hashFiles('**/Cargo.lock') }}-${{ matrix.python-version }} 67 | 68 | # Cache `cargo install` built binaries 69 | - name: "Cache Built Binaries" 70 | id: "cache-binaries" 71 | uses: "actions/cache@v4" 72 | with: 73 | path: "~/.cargo/bin" 74 | # In theory, this should rebuild binaries once a month 75 | key: "${{ runner.os }}-cargo-binaries-${{steps.get-date.outputs.date}}" 76 | 77 | - name: Install wasm-pack 78 | uses: jetli/wasm-pack-action@v0.4.0 79 | with: 80 | version: "v0.12.1" 81 | 82 | - name: "Run Tests" 83 | if: "${{ !contains(runner.os, 'windows') }}" 84 | shell: bash 85 | run: "cargo test --all-features" 86 | 87 | - name: "Run Tests (Windows)" 88 | if: "${{ contains(runner.os, 'windows') }}" 89 | shell: bash 90 | # Python behaves weirdly with setup.py develop in Windows, 91 | # when it comes to loading DLLs, so on that platform we build and 92 | # install the wheel and run the tests with that. 93 | # Running `cargo test --features=wasm` runs all the regular lib 94 | # tests plus the WASM integration tests, while excluding the 95 | # python integration tests 96 | run: | 97 | cargo test --features=wasm 98 | make develop-py-wheel 99 | ls dist/*.whl 100 | pip install dist/*.whl 101 | echo "Running Tests" 102 | python tests/test_py.py 103 | env: 104 | WINDOWS: "${{ contains(runner.os, 'windows') }}" 105 | PYTHON: ${{ steps.get-py-path.outputs.path }} 106 | 107 | build: 108 | name: "Build Libs, WASM, and Python sdist" 109 | needs: "test" 110 | runs-on: ubuntu-latest 111 | steps: 112 | # Check out the code 113 | - uses: "actions/checkout@v4" 114 | 115 | # Install python 116 | - name: "Set up python" 117 | uses: "actions/setup-python@v5" 118 | with: 119 | python-version: "3.12" 120 | 121 | - name: "Get Python Path" 122 | id: get-py-path 123 | shell: bash 124 | run: | 125 | echo "path=$(which python)" >> $GITHUB_OUTPUT 126 | 127 | # Set the current month and year (used for cache key) 128 | - name: "Get Date" 129 | id: get-date 130 | # Outputs e.g. "202007" 131 | # tbh I have yet to find the docs where this output format is 132 | # defined, but I copied this from the official cache action's README. 133 | run: | 134 | echo "date=$(/bin/date -u '+%Y%m')" >> $GITHUB_OUTPUT 135 | shell: bash 136 | 137 | # Generate the lockfile 138 | - name: "Generate Cargo Lockfile" 139 | run: "cargo generate-lockfile" 140 | 141 | # Cache build dependencies 142 | - name: "Cache Build Fragments" 143 | id: "cache-build-fragments" 144 | uses: "actions/cache@v4" 145 | with: 146 | path: | 147 | ~/.cargo/registry 148 | ~/.cargo/git 149 | target 150 | # This should partial match the caches generated for the tests, 151 | # which include a python version at the end. 152 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 153 | 154 | # Cache `cargo install` built binaries 155 | - name: "Cache Built Binaries" 156 | id: "cache-binaries" 157 | uses: "actions/cache@v4" 158 | with: 159 | path: "~/.cargo/bin" 160 | # In theory, this should rebuild binaries once a month 161 | key: "${{ runner.os }}-cargo-binaries-${{steps.get-date.outputs.date}}" 162 | 163 | - name: Install wasm-pack 164 | uses: jetli/wasm-pack-action@v0.4.0 165 | with: 166 | version: "v0.12.1" 167 | 168 | - name: "Build Rust/C Libraries" 169 | run: "make build" 170 | 171 | - name: "Check Rust target content" 172 | run: ls target/release 173 | 174 | - uses: actions/upload-artifact@v4 175 | name: "Upload Rust/C Libraries" 176 | with: 177 | path: target/release/libjsonlogic_rs.* 178 | name: libs 179 | 180 | - name: "Build Python Source Dist" 181 | run: "make build-py-sdist" 182 | env: 183 | WINDOWS: "${{ contains(runner.os, 'windows') }}" 184 | PYTHON: ${{ steps.get-py-path.outputs.path }} 185 | 186 | - uses: actions/upload-artifact@v4 187 | name: "Upload Python sdist" 188 | with: 189 | path: dist/*.tar.gz 190 | name: py-sdist 191 | 192 | - name: "Build WASM Node Package" 193 | run: "make build-wasm" 194 | 195 | - uses: actions/upload-artifact@v4 196 | name: "Upload node package" 197 | with: 198 | path: js/ 199 | name: wasm-pkg 200 | 201 | build-wheels: 202 | name: > 203 | Build Wheel 204 | (${{ matrix.platform.os }}, ${{ matrix.platform.name }}, ${{ matrix.py }}) 205 | # needs: test 206 | runs-on: ${{ matrix.platform.os }} 207 | strategy: 208 | fail-fast: false 209 | matrix: 210 | platform: 211 | - { os: ubuntu-latest, name: manylinux } 212 | - { os: ubuntu-latest, name: musllinux } 213 | - { os: macos-latest, name: macosx } 214 | py: 215 | - cp39 216 | - cp310 217 | - cp311 218 | - cp312 219 | - cp313 220 | 221 | steps: 222 | - uses: actions/checkout@v4 223 | 224 | - name: Set up QEMU 225 | if: runner.os == 'Linux' 226 | uses: docker/setup-qemu-action@v3 227 | with: 228 | platforms: all 229 | 230 | - name: Build wheels 231 | uses: pypa/cibuildwheel@v2.22.0 232 | env: 233 | # configure cibuildwheel to build native archs ('auto'), and some 234 | # emulated ones 235 | CIBW_ARCHS_LINUX: x86_64 aarch64 236 | # cross-compiling wheels for discrete architectures not working OOTB 237 | # see: https://github.com/PyO3/setuptools-rust/issues/206 238 | CIBW_ARCHS_MACOS: x86_64 universal2 arm64 239 | CIBW_ARCHS_WINDOWS: AMD64 240 | CIBW_BEFORE_ALL_LINUX: > 241 | curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain stable -y 242 | CIBW_BEFORE_ALL_MACOS: | 243 | rustup target add aarch64-apple-darwin 244 | rustup target add x86_64-apple-darwin 245 | CIBW_ENVIRONMENT_LINUX: PATH=/root/.cargo/bin:$PATH 246 | CIBW_ENVIRONMENT_MACOS: MACOSX_DEPLOYMENT_TARGET=10.12 247 | CIBW_BUILD: ${{ matrix.py }}-${{ matrix.platform.name }}_* 248 | CIBW_TEST_COMMAND: python {project}/tests/test_py.py 249 | 250 | - uses: actions/upload-artifact@v4 251 | with: 252 | name: py-wheels-${{ matrix.platform.name }}-${{ matrix.py }} 253 | path: ./wheelhouse/*.whl 254 | 255 | distribute: 256 | name: "Distribute Cargo, WASM, and Python Sdist Packages" 257 | needs: ["build", "build-wheels"] 258 | runs-on: ubuntu-latest 259 | if: github.event_name == 'release' && github.event.action == 'published' 260 | steps: 261 | # Check out the code 262 | - uses: "actions/checkout@v4" 263 | 264 | # Install python 265 | - name: "Set up python" 266 | uses: "actions/setup-python@v5" 267 | with: 268 | python-version: "3.12" 269 | 270 | # Generate the lockfile 271 | - name: "Generate Cargo Lockfile" 272 | run: "cargo generate-lockfile" 273 | 274 | - name: "Get Current Version" 275 | id: get-version 276 | shell: bash 277 | run: | 278 | echo "version=$(cargo pkgid | tr '#' '\n' | tail -n 1 | tr ':' ' ' | awk '{print $2}')" >> $GITHUB_OUTPUT 279 | 280 | - name: "(DEBUG) log current version" 281 | shell: bash 282 | run: | 283 | echo "${{ steps.get-version.outputs.version }}" 284 | 285 | - name: "Check if new Cargo version" 286 | id: cargo-version 287 | shell: bash 288 | run: | 289 | echo "new=$(./scripts/newCargoVersion.sh)" >> $GITHUB_OUTPUT 290 | 291 | - name: "Check if new NPM version" 292 | id: npm-version 293 | shell: bash 294 | run: | 295 | echo "new=$(./scripts/newNpmVersion.sh)" >> $GITHUB_OUTPUT 296 | 297 | # Note we don't check for a new python version b/c there are so 298 | # many python artifacts that it is impractical. Instead we just 299 | # upload with a `--skip-existing` flag, so if it's already there 300 | # it wont' be an error. 301 | 302 | - name: "(DEBUG) new versions" 303 | shell: bash 304 | run: | 305 | echo "Cargo: ${{ steps.cargo-version.outputs.new }}" 306 | echo "NPM: ${{ steps.npm-version.outputs.new }}" 307 | 308 | - name: "Persist new cargo state for subsequent jobs" 309 | shell: bash 310 | run: | 311 | echo "${{ steps.cargo-version.outputs.new }}" > tmp-new-cargo-ver 312 | 313 | - uses: actions/upload-artifact@v4 314 | with: 315 | path: "tmp-new-cargo-ver" 316 | name: "new-cargo" 317 | 318 | - name: "Cargo Publish" 319 | if: "${{ steps.cargo-version.outputs.new == 'true' }}" 320 | run: | 321 | cargo publish --token "$CARGO_TOKEN" 322 | env: 323 | CARGO_TOKEN: "${{ secrets.CARGO_TOKEN }}" 324 | 325 | - name: "Pull WASM Artifact" 326 | uses: actions/download-artifact@v4 327 | if: "${{ steps.npm-version.outputs.new == 'true' }}" 328 | with: 329 | name: wasm-pkg 330 | path: dist-wasm 331 | 332 | - name: "Publish NPM Package" 333 | shell: bash 334 | if: "${{ steps.npm-version.outputs.new == 'true' }}" 335 | run: | 336 | echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc 337 | npm publish dist-wasm/ --access public 338 | env: 339 | NPM_TOKEN: "${{ secrets.NPM_TOKEN }}" 340 | 341 | - name: "Pull Python Sdist Artifact" 342 | if: "${{ steps.cargo-version.outputs.new == 'true' }}" 343 | uses: actions/download-artifact@v4 344 | with: 345 | name: py-sdist 346 | path: dist-py 347 | 348 | - name: "Publish Python Sdist" 349 | if: "${{ steps.cargo-version.outputs.new == 'true' }}" 350 | shell: bash 351 | run: | 352 | pip install twine 353 | twine upload --skip-existing dist-py/* 354 | env: 355 | TWINE_USERNAME: "__token__" 356 | TWINE_PASSWORD: "${{ secrets.PYPI_TOKEN }}" 357 | 358 | distribute-py-wheels: 359 | name: "Distribute Python Wheels" 360 | needs: ["distribute"] 361 | runs-on: ubuntu-latest 362 | # upload to PyPI on every tag starting with 'v' 363 | # if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v') 364 | # alternatively, to publish when a GitHub Release is created, use the following rule: 365 | if: github.event_name == 'release' && github.event.action == 'published' 366 | steps: 367 | - uses: actions/download-artifact@v4 368 | with: 369 | path: dist 370 | pattern: py-wheels-* 371 | merge-multiple: true 372 | 373 | - uses: pypa/gh-action-pypi-publish@release/v1 374 | with: 375 | user: __token__ 376 | password: ${{ secrets.PYPI_TOKEN }} 377 | skip_existing: true 378 | # To test: repository_url: https://test.pypi.org/legacy/ 379 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | 13 | #Added by cargo 14 | # 15 | #already existing elements are commented out 16 | 17 | /target 18 | #**/*.rs.bk 19 | #Cargo.lock 20 | 21 | **/.DS_Store 22 | 23 | js/ 24 | 25 | **/__pycache__/ 26 | **/*.egg-info/ 27 | build/ 28 | dist/ 29 | venv/ 30 | 31 | # Used in the CI pipeline 32 | tmp-new-cargo-ver 33 | new-cargo/ 34 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 88 -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb", 9 | "request": "launch", 10 | "name": "Debug unit tests in library 'jsonlogic'", 11 | "cargo": { 12 | "args": [ 13 | "test", 14 | "--no-run", 15 | "--lib", 16 | "--package=jsonlogic" 17 | ] 18 | }, 19 | "args": [], 20 | "cwd": "${workspaceFolder}", 21 | "sourceLanguages": [ 22 | "rust" 23 | ] 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "python.venvPath": "venv", 4 | "python.pythonPath": "python", 5 | "python.formatting.provider": "black", 6 | "python.linting.enabled": true, 7 | "python.linting.mypyEnabled": true, 8 | "python.linting.flake8Enabled": true, 9 | "python.linting.pylintEnabled": true, 10 | "python.linting.pydocstyleEnabled": true, 11 | "rust-analyzer.cargo.allFeatures": true, 12 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.2.1] - 2020-08-17 10 | 11 | ### Changed 12 | 13 | - `in` will now accept `null` or anything that evaluates to `null` for its 14 | second argument 15 | 16 | ## [0.2.0] - 2020-08-17 17 | 18 | ### Added 19 | 20 | - A new `cmdline` feature that builds a `jsonlogic` binary for JsonLogic on 21 | the commandline 22 | 23 | ### Changed 24 | 25 | - `all`, `some`, and `none` will now accept an initial argument (the iterator) 26 | that is or evaluates to `null`. 27 | 28 | ## [0.1.3] - 2020-07-15 29 | 30 | - More minor CI fixes 31 | 32 | ## [0.1.2] - 2020-07-14 33 | 34 | ### Chore 35 | 36 | - A bunch of minor fixes to get the CI pipeline working for all platforms 37 | 38 | ## [0.1.1] - 2020-07-14 39 | 40 | ### Fixed 41 | - The Python source dist wasn't generating a Cargo lockfile prior to attempting 42 | to determine the package version, causing the `cargo pkgid` command to fail 43 | 44 | ### Chore 45 | - CI fixes for distribution of all the python wheels 46 | - Bumped version to test distribution pipeline 47 | 48 | ### Docs 49 | - Installation instructions in README 50 | 51 | ## [0.1.0] - 2020-07-05 52 | 53 | ### Added 54 | - All standard JSONLogic operations 55 | - WASM build 56 | - Python SDist build 57 | - Packages published & registered on the various package repositories 58 | 59 | [Unreleased]: https://github.com/Bestowinc/json-logic-rs/compare/v0.2.0...HEAD 60 | [0.2.0]: https://github.com/Bestowinc/json-logic-rs/compare/v0.1.3...v0.2.0 61 | [0.1.3]: https://github.com/Bestowinc/json-logic-rs/compare/v0.1.2...v0.1.3 62 | [0.1.2]: https://github.com/Bestowinc/json-logic-rs/compare/v0.1.1...v0.1.2 63 | [0.1.1]: https://github.com/Bestowinc/json-logic-rs/compare/v0.1.0...v0.1.1 64 | [0.1.0]: https://github.com/Bestowinc/json-logic-rs/compare/0ce0196...v0.1.0 65 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Matthew Planchard "] 3 | categories = ["parsing", "wasm", "web-programming"] 4 | description = "jsonlogic (jsonlogic.com) implemented in Rust" 5 | edition = "2018" 6 | homepage = "https://github.com/bestowinc/json-logic-rs" 7 | keywords = ["json", "jsonlogic", "s-expressions", "web", "logic"] 8 | license = "MIT" 9 | name = "jsonlogic-rs" 10 | readme = "README.md" 11 | repository = "https://github.com/bestowinc/json-logic-rs" 12 | version = "0.5.0" 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [lib] 17 | # cdylib for CFFI and python integration 18 | # lib for regular rust stuff 19 | crate-type = ["cdylib", "lib"] 20 | 21 | [[bin]] 22 | name = "jsonlogic" 23 | path = "src/bin.rs" 24 | required-features = ["cmdline"] 25 | 26 | [features] 27 | cmdline = ["anyhow", "clap"] 28 | default = [] 29 | python = ["cpython"] 30 | wasm = ["wasm-bindgen"] 31 | 32 | [dependencies] 33 | phf = {version = "~0.8.0", features = ["macros"]} 34 | serde_json = "~1.0.41" 35 | thiserror = "~1.0.11" 36 | 37 | [dependencies.wasm-bindgen] 38 | features = ["serde-serialize"] 39 | optional = true 40 | version = "~0.2.62" 41 | 42 | [dependencies.cpython] 43 | features = ["extension-module"] 44 | optional = true 45 | version = "0.7" 46 | 47 | [dependencies.anyhow] 48 | optional = true 49 | version = "~1.0.31" 50 | 51 | [dependencies.clap] 52 | optional = true 53 | version = "~2.33.1" 54 | 55 | [dev-dependencies.reqwest] 56 | features = ["blocking"] 57 | version = "~0.10.6" 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Matthew Planchard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include src * 2 | include Cargo.toml 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # TODO: split sub-language makes into their dirs & call `$(MAKE) -C dir` for them 2 | 3 | SHELL = bash 4 | 5 | ifeq ($(WINDOWS),true) 6 | VENV=venv/Scripts/python.exe 7 | else 8 | VENV=venv/bin/python 9 | endif 10 | 11 | ifeq ($(PYTHON),) 12 | PYTHON := python$(PY_VER) 13 | endif 14 | 15 | 16 | .PHONY: build 17 | build: 18 | cargo build --release 19 | 20 | .PHONY: build-wasm 21 | build-wasm: setup 22 | cargo clean -p jsonlogic-rs 23 | rm -rf ./js && wasm-pack build --target nodejs --out-dir js --out-name index --release --scope bestow -- --features wasm 24 | 25 | .PHONY: debug-wasm 26 | debug-wasm: 27 | rm -rf ./js && wasm-pack build --target nodejs --out-dir js --out-name index --debug --scope bestow -- --features wasm 28 | 29 | .PHONY: clean-py 30 | clean-py: 31 | rm -rf build/* 32 | rm -rf dist/* 33 | 34 | .PHONY: build-py-sdist 35 | build-py-sdist: $(VENV) clean-py 36 | cargo clean -p jsonlogic-rs 37 | $(VENV) setup.py sdist 38 | 39 | .PHONY: build-py-wheel 40 | build-py-wheel: $(VENV) clean-py 41 | cargo clean -p jsonlogic-rs 42 | $(VENV) setup.py bdist_wheel 43 | 44 | # NOTE: this command may require sudo on linux 45 | .PHONY: build-py-wheel-manylinux 46 | build-py-wheel-manylinux: clean-py 47 | docker run -v "$$PWD":/io --rm "$(MANYLINUX_IMG)" /io/build-wheels.sh 48 | 49 | # NOTE: this command may require sudo on linux 50 | .PHONY: build-py-wheel-manylinux-no-clean 51 | build-py-wheel-manylinux-no-clean: 52 | docker run -v "$$PWD":/io --rm "$(MANYLINUX_IMG)" /io/build-wheels.sh 53 | 54 | .PHONY: build-py-all 55 | build-py-all: $(VENV) clean-py 56 | cargo clean -p jsonlogic-rs 57 | $(VENV) setup.py sdist bdist_wheel 58 | 59 | .PHONY: develop-py-wheel 60 | develop-py-wheel: $(VENV) 61 | $(VENV) setup.py bdist_wheel 62 | 63 | .PHONY: develop-py 64 | develop-py: $(VENV) 65 | $(VENV) setup.py develop 66 | 67 | .PHONY: distribute-py 68 | distribute-py: $(VENV) 69 | $(VENV) -m pip install twine 70 | twine upload -s dist/* 71 | 72 | .PHONY: test-distribute-py 73 | test-distribute-py: 74 | $(VENV) -m pip install twine 75 | twine upload -s --repository testpypi dist/* 76 | 77 | .PHONY: setup 78 | setup: 79 | wasm-pack --version > /dev/null 2>&1 || cargo install wasm-pack 80 | 81 | .PHONY: test 82 | test: 83 | PYTHON=$(PYTHON) WINDOWS=$(WINDOWS) cargo test --all-features 84 | 85 | .PHONY: test-wasm 86 | test-wasm: 87 | node tests/test_wasm.js 88 | 89 | .PHONY: test-py 90 | test-py: $(VENV) 91 | $(VENV) tests/test_py.py 92 | 93 | # Note: please change both here and in the build-wheels script if specifying a 94 | # particular version or removing the version pin. setuptools-rust is currently 95 | # pinned because the windows builds were broken with v0.11.3. 96 | venv: $(VENV) 97 | $(VENV): setup.py pyproject.toml 98 | $(PYTHON) -m venv venv 99 | $(VENV) -m pip install setuptools wheel setuptools-rust==0.10.6 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # json-logic-rs 2 | 3 | ![Continuous Integration](https://github.com/Bestowinc/json-logic-rs/workflows/Continuous%20Integration/badge.svg?branch=master) 4 | 5 | This is an implementation of the [JsonLogic] specification in Rust. 6 | 7 | ## Project Status 8 | 9 | We implement 100% of the standard supported operations defined [here](http://jsonlogic.com/operations.html). 10 | 11 | We also implement the `?:`, which is not described in that specification 12 | but is a direct alias for `if`. 13 | 14 | All operations are tested using our own test suite in Rust as well as the 15 | shared tests for all JsonLogic implementations defined [here](http://jsonlogic.com/tests.json). 16 | 17 | We are working on adding new operations with improved type safety, as well 18 | as the ability to define functions as JsonLogic. We will communicate with 19 | the broader JsonLogic community to see if we can make them part of the 20 | standard as we do so. 21 | 22 | Being built in Rust, we are able to provide the package in a variety of 23 | languages. The table below describes current language support: 24 | 25 | | **Language** | **Available Via** | 26 | | -------------------- | -------------------------------------------------------------------------- | 27 | | Rust | [Cargo](https://crates.io/crates/jsonlogic-rs) | 28 | | JavaScript (as WASM) | Node Package via [NPM](https://www.npmjs.com/package/@bestow/jsonlogic-rs) | 29 | | Python | [PyPI](https://pypi.org/project/jsonlogic-rs/) | 30 | 31 | ## Installation 32 | 33 | ### Rust 34 | 35 | To use as a Rust library, add to your `Cargo.toml`: 36 | 37 | ``` toml 38 | [dependencies] 39 | jsonlogic-rs = "~0.1" 40 | ``` 41 | 42 | If you just want to use the commandline `jsonlogic` binary: 43 | 44 | ``` sh 45 | cargo install jsonlogic-rs --features cmdline 46 | ``` 47 | 48 | ### Node/Browser 49 | 50 | You can install JsonLogic using npm or yarn. In NPM: 51 | 52 | ``` sh 53 | npm install --save @bestow/jsonlogic-rs 54 | ``` 55 | 56 | Note that the package is distributed as a node package, so you'll need to use 57 | `browserify`, `webpack`, or similar to install for the browser. 58 | 59 | ### Python 60 | 61 | Supports Python 3.7+. 62 | 63 | Wheels are distributed for many platforms, so you can often just run: 64 | 65 | ``` sh 66 | pip install jsonlogic-rs 67 | ``` 68 | 69 | If a wheel does _not_ exist for your system, this will attempt to build the 70 | package. In order for the package to build successfully, you MUST have Rust 71 | installed on your local system, and `cargo` MUST be present in your `PATH`. 72 | 73 | See [Building](#Building) below for more details. 74 | 75 | ## Usage 76 | 77 | ### Rust 78 | 79 | ```rust 80 | use jsonlogic_rs; 81 | use serde_json::{json, from_str, Value}; 82 | 83 | // You can pass JSON values deserialized with serde straight into apply(). 84 | fn main() { 85 | let data: Value = from_str(r#"{"a": 7}"#) 86 | assert_eq!( 87 | jsonlogic_rs::apply( 88 | json!({"===": [{"var": "a"}, 7]}), 89 | data, 90 | ), 91 | json!(true) 92 | ); 93 | } 94 | ``` 95 | 96 | ### Javascript 97 | 98 | ```js 99 | const jsonlogic = require("jsonlogic-rs") 100 | 101 | jsonlogic.apply( 102 | {"===": [{"var": "a"}, 7]}, 103 | {"a": 7} 104 | ) 105 | ``` 106 | 107 | ### Python 108 | 109 | ```py 110 | import jsonlogic_rs 111 | 112 | res = jsonlogic_rs.apply( 113 | {"===": [{"var": "a"}, 7]}, 114 | {"a": 7} 115 | ) 116 | 117 | assert res == True 118 | 119 | # If You have serialized JsonLogic and data, the `apply_serialized` method can 120 | # be used instead 121 | res = jsonlogic_rs.apply_serialized( 122 | '{"===": [{"var": "a"}, 7]}', 123 | '{"a": 7}' 124 | ) 125 | ``` 126 | 127 | ### Commandline 128 | 129 | ``` raw 130 | Parse JSON data with a JsonLogic rule. 131 | 132 | When no or is -, read from stdin. 133 | 134 | The result is written to stdout as JSON, so multiple calls 135 | can be chained together if desired. 136 | 137 | USAGE: 138 | jsonlogic [data] 139 | 140 | FLAGS: 141 | -h, --help Prints help information 142 | -V, --version Prints version information 143 | 144 | ARGS: 145 | A JSON logic string 146 | A string of JSON data to parse. May be provided as stdin. 147 | 148 | EXAMPLES: 149 | jsonlogic '{"===": [{"var": "a"}, "foo"]}' '{"a": "foo"}' 150 | jsonlogic '{"===": [1, 1]}' null 151 | echo '{"a": "foo"}' | jsonlogic '{"===": [{"var": "a"}, "foo"]}' 152 | 153 | Inspired by and conformant with the original JsonLogic (jsonlogic.com). 154 | ``` 155 | 156 | Run `jsonlogic --help` the most up-to-date usage. 157 | 158 | An example of chaining multiple results: 159 | 160 | ``` sh 161 | $ echo '{"a": "a"}' \ 162 | | jsonlogic '{"if": [{"===": [{"var": "a"}, "a"]}, {"result": true}, {"result": false}]}' \ 163 | | jsonlogic '{"if": [{"!!": {"var": "result"}}, "result was true", "result was false"]}' 164 | 165 | "result was true" 166 | ``` 167 | 168 | Using `jsonlogic` on the cmdline to explore an API: 169 | 170 | ``` sh 171 | > curl -s "https://catfact.ninja/facts?limit=5" 172 | 173 | {"current_page":1,"data":[{"fact":"The Egyptian Mau is probably the oldest breed of cat. In fact, the breed is so ancient that its name is the Egyptian word for \u201ccat.\u201d","length":132},{"fact":"Julius Ceasar, Henri II, Charles XI, and Napoleon were all afraid of cats.","length":74},{"fact":"Unlike humans, cats cannot detect sweetness which likely explains why they are not drawn to it at all.","length":102},{"fact":"Cats can be taught to walk on a leash, but a lot of time and patience is required to teach them. The younger the cat is, the easier it will be for them to learn.","length":161},{"fact":"Researchers believe the word \u201ctabby\u201d comes from Attabiyah, a neighborhood in Baghdad, Iraq. Tabbies got their name because their striped coats resembled the famous wavy patterns in the silk produced in this city.","length":212}],"first_page_url":"https:\/\/catfact.ninja\/facts?page=1","from":1,"last_page":67,"last_page_url":"https:\/\/catfact.ninja\/facts?page=67","next_page_url":"https:\/\/catfact.ninja\/facts?page=2","path":"https:\/\/catfact.ninja\/facts","per_page":"5","prev_page_url":null,"to":5,"total":332} 174 | 175 | > curl -s "https://catfact.ninja/facts?limit=5" | jsonlogic '{"var": "data"}' 176 | 177 | [{"fact":"A cat's appetite is the barometer of its health. Any cat that does not eat or drink for more than two days should be taken to a vet.","length":132},{"fact":"Some notable people who disliked cats: Napoleon Bonaparte, Dwight D. Eisenhower, Hitler.","length":89},{"fact":"During the time of the Spanish Inquisition, Pope Innocent VIII condemned cats as evil and thousands of cats were burned. Unfortunately, the widespread killing of cats led to an explosion of the rat population, which exacerbated the effects of the Black Death.","length":259},{"fact":"A cat has approximately 60 to 80 million olfactory cells (a human has between 5 and 20 million).","length":96},{"fact":"In just seven years, a single pair of cats and their offspring could produce a staggering total of 420,000 kittens.","length":115}] 178 | 179 | > curl -s "https://catfact.ninja/facts?limit=5" | jsonlogic '{"var": "data.0"}' 180 | 181 | {"fact":"A tiger's stripes are like fingerprints","length":39} 182 | 183 | > curl -s "https://catfact.ninja/facts?limit=5" | jsonlogic '{"var": "data.0.fact"}' 184 | "Neutering a male cat will, in almost all cases, stop him from spraying (territorial marking), fighting with other males (at least over females), as well as lengthen his life and improve its quality." 185 | 186 | > curl -s "https://catfact.ninja/facts?limit=5" \ 187 | | jsonlogic '{"var": "data.0.fact"}' \ 188 | | jsonlogic '{"in": ["cat", {"var": ""}]}' 189 | 190 | true 191 | 192 | > curl -s "https://catfact.ninja/facts?limit=5" \ 193 | | jsonlogic '{"var": "data.0.fact"}' \ 194 | | jsonlogic '{"in": ["cat", {"var": ""}]}' \ 195 | | jsonlogic '{"if": [{"var": ""}, "fact contained cat", "fact did not contain cat"]}' 196 | 197 | "fact contained cat" 198 | ``` 199 | 200 | ## Building 201 | 202 | ### Prerequisites 203 | 204 | You must have Rust installed and `cargo` available in your `PATH`. 205 | 206 | If you would like to build or test the Python distribution, Python 3.7 or 207 | newer must be available in your `PATH`. The `venv` module must be part of the 208 | Python distribution (looking at you, Ubuntu). 209 | 210 | If you would like to run tests for the WASM package, `node` 10 or newer must be 211 | available in your `PATH`. 212 | 213 | ### Rust 214 | 215 | To build the Rust library, just run `cargo build`. 216 | 217 | You can create a release build with `make build`. 218 | 219 | ### WebAssembly 220 | 221 | You can build a debug WASM release with 222 | 223 | ```sh 224 | make debug-wasm 225 | ``` 226 | 227 | You can build a production WASM release with 228 | 229 | ```sh 230 | make build-wasm 231 | ``` 232 | 233 | The built WASM package will be in `js/`. This package is directly importable 234 | from `node`, but needs to be browserified in order to be used in the browser. 235 | 236 | ### Python 237 | 238 | To perform a dev install of the Python package, run: 239 | 240 | ```sh 241 | make develop-py 242 | ``` 243 | 244 | This will automatically create a virtual environment in `venv/`, install 245 | the necessary packages, and then install `jsonlogic_rs` into that environment. 246 | 247 | **Note:** from our CI experiences, this may not work for Python 3.8 on Windows. 248 | If you are running this on a Windows machine and can confirm whether or not 249 | this works, let us know! 250 | 251 | To build a production source distribution: 252 | 253 | ```sh 254 | make build-py-sdist 255 | ``` 256 | 257 | To build a wheel (specific to your current system architecture and python 258 | version): 259 | 260 | ```sh 261 | make build-py-wheel 262 | ``` 263 | 264 | The python distribution consists both of the C extension generated from the 265 | Rust and a thin wrapper found in `py/jsonlogic_rs/`. `make develop-py` will 266 | compile the C extension and place it in that directory, where it will be 267 | importable by your local venv. When building wheels, the wrapper and the C 268 | extension are all packaged together into the resultant wheel, which will 269 | be found in `dist/`. When building an sdist, the Rust extension is not compiled. 270 | The Rust and Python source are distributed together in a `.tar.gz` file, again 271 | found in `dist/`. 272 | 273 | [jsonlogic]: http://jsonlogic.com/ 274 | -------------------------------------------------------------------------------- /build-wheels.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Expected to be run in a manylinux container 4 | 5 | set -ex 6 | 7 | cd /io 8 | 9 | curl https://sh.rustup.rs -sSf | 10 | sh -s -- --default-toolchain stable -y 11 | 12 | export PATH=/root/.cargo/bin:$PATH 13 | 14 | mkdir -p build && rm -rf build/* 15 | 16 | for PYBIN in /opt/python/{cp39-cp39,cp310-cp310,cp311-cp311,cp312-cp312,cp-313-cp313}/bin; do 17 | export PYTHON_SYS_EXECUTABLE="$PYBIN/python" 18 | 19 | "${PYBIN}/python" -m ensurepip 20 | # Note: please change both here and in the makefile if specifying a particular 21 | # version or removing the version pin. 22 | "${PYBIN}/python" -m pip install -U setuptools wheel setuptools-rust==1.6.0 23 | "${PYBIN}/python" setup.py bdist_wheel 24 | done 25 | 26 | for whl in dist/*.whl; do 27 | auditwheel repair "$whl" -w dist/ 28 | done 29 | -------------------------------------------------------------------------------- /py/jsonlogic_rs/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Ignore development build of C extension 3 | *.so 4 | -------------------------------------------------------------------------------- /py/jsonlogic_rs/__init__.py: -------------------------------------------------------------------------------- 1 | """Python JSONLogic with a Rust Backend.""" 2 | 3 | __all__ = ( 4 | "apply", 5 | "apply_serialized", 6 | ) 7 | 8 | import json as _json 9 | import sys as _sys 10 | 11 | try: 12 | from .jsonlogic import apply as _apply 13 | except ImportError: 14 | # See https://docs.python.org/3/library/os.html#os.add_dll_directory 15 | # for why this is here. 16 | if _sys.platform.startswith("win"): 17 | import os 18 | from pathlib import Path 19 | if hasattr(os, "add_dll_directory"): 20 | os.add_dll_directory(str(Path(__file__).parent)) 21 | from .jsonlogic import apply as _apply 22 | else: 23 | raise 24 | 25 | 26 | def apply(value, data=None, serializer=None, deserializer=None): 27 | """Run JSONLogic on a value and some data.""" 28 | serializer = serializer if serializer is not None else _json.dumps 29 | deserializer = deserializer if deserializer is not None else _json.loads 30 | res = _apply(serializer(value), serializer(data)) 31 | return deserializer(res) 32 | 33 | 34 | def apply_serialized(value: str, data: str = None, deserializer=None): 35 | """Run JSONLogic on some already serialized value and optional data.""" 36 | res = _apply(value, data if data is not None else "null") 37 | return deserializer(res) 38 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel", "setuptools-rust>=1.2.0"] 3 | 4 | [tool.black] 5 | line-length = 80 6 | target-version = ['py39'] 7 | -------------------------------------------------------------------------------- /scripts/build-all-linux-wheels.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | # Build wheels for each of the manylinux specifications. 5 | 6 | TARGETS="quay.io/pypa/manylinux1_i686:2020-07-04-283458f " 7 | TARGETS+="quay.io/pypa/manylinux1_x86_64:2020-07-04-283458f " 8 | TARGETS+="quay.io/pypa/manylinux2010_i686:2020-07-04-10a3c30 " 9 | TARGETS+="quay.io/pypa/manylinux2010_x86_64:2020-07-04-10a3c30 " 10 | TARGETS+="quay.io/pypa/manylinux2014_i686:2020-07-04-bb5f087 " 11 | TARGETS+="quay.io/pypa/manylinux2014_x86_64:2020-07-04-bb5f087 " 12 | 13 | for TARGET in ${TARGETS}; do 14 | MANYLINUX_IMG="${TARGET}" make build-py-wheel-manylinux-no-clean; 15 | sleep 5; 16 | done 17 | -------------------------------------------------------------------------------- /scripts/newCargoVersion.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | CURRENT_VERSION=$(cargo pkgid | tr '#' '\n' | tail -n 1 | tr ':' ' ' | awk '{print $2}') 5 | 6 | RESP=$(curl 'https://crates.io/api/v1/crates/jsonlogic-rs' -s \ 7 | -H 'User-Agent: mplanchard_verison_check (msplanchard@gmail.com)' \ 8 | -H 'Accept: application/json' \ 9 | -H 'Cache-Control: max-age=0') 10 | 11 | PREV_VERSION=$(echo "${RESP}" \ 12 | | tr ',' '\n' \ 13 | | grep newest_version \ 14 | | tr ':' ' ' \ 15 | | awk '{print $2}' \ 16 | | sed 's/"//g') 17 | 18 | if [[ "${CURRENT_VERSION}" == "${PREV_VERSION}" ]]; then 19 | echo false 20 | exit 0 21 | else 22 | echo true 23 | exit 0 24 | fi 25 | -------------------------------------------------------------------------------- /scripts/newNpmVersion.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | DIST_VERSION=$(npm view @bestow/jsonlogic-rs version) 5 | 6 | CURRENT_VERSION=$(cargo pkgid | tr ':' ' ' | awk '{print $3}') 7 | 8 | 9 | if [[ "${CURRENT_VERSION}" == "${DIST_VERSION}" ]]; then 10 | echo false 11 | exit 0 12 | else 13 | echo true 14 | exit 0 15 | fi 16 | -------------------------------------------------------------------------------- /scripts/newPypiVersion.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | DIST_VERSION=$(pip search jsonlogic-rs | grep -e '^jsonlogic-rs (' | awk '{print $2}' | sed 's/[\(\)]//g') 5 | 6 | CURRENT_VERSION=$(cargo pkgid | tr ':' ' ' | awk '{print $3}') 7 | 8 | if [[ "${CURRENT_VERSION}" == "${DIST_VERSION}" ]]; then 9 | echo false 10 | exit 0 11 | else 12 | echo true 13 | exit 0 14 | fi 15 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 80 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from subprocess import PIPE, Popen 3 | 4 | from setuptools import setup 5 | from setuptools_rust import Binding, RustExtension 6 | 7 | PKG_ROOT = Path(__file__).parent 8 | SETUP_REQUIRES = ["setuptools-rust", "wheel", "setuptools"] 9 | SHORT_DESCRIPTION = "JsonLogic implemented with a Rust backend" 10 | URL = "https://www.github.com/bestowinc/json-logic-rs" 11 | AUTHOR = "Matthew Planchard" 12 | EMAIL = "msplanchard@gmail.com" 13 | 14 | 15 | def generate_lockfile(): 16 | if (PKG_ROOT / "Cargo.lock").exists(): 17 | return 18 | print("Generating Cargo lockfile") 19 | proc = Popen(("cargo", "generate-lockfile"), stdout=PIPE, stderr=PIPE) 20 | _out, err = tuple(map(bytes.decode, proc.communicate())) 21 | if proc.returncode != 0: 22 | raise RuntimeError(f"Could not generate Cargo lockfile: {err}") 23 | return 24 | 25 | 26 | def get_version(): 27 | generate_lockfile() 28 | proc = Popen(("cargo", "pkgid"), stdout=PIPE, stderr=PIPE) 29 | out, err = tuple(map(bytes.decode, proc.communicate())) 30 | if proc.returncode != 0: 31 | raise RuntimeError(f"Could not get Cargo package info: {err}") 32 | version = out.split("@")[-1] 33 | return version.strip() 34 | 35 | 36 | with open(PKG_ROOT / "README.md") as readme_f: 37 | LONG_DESCRIPTION = readme_f.read() 38 | 39 | VERSION = get_version() 40 | 41 | 42 | setup( 43 | name="jsonlogic-rs", 44 | author=AUTHOR, 45 | version=VERSION, 46 | author_email=EMAIL, 47 | maintainer_email=EMAIL, 48 | url=URL, 49 | description=SHORT_DESCRIPTION, 50 | long_description=LONG_DESCRIPTION, 51 | long_description_content_type="text/markdown", 52 | keywords=["json", "jsonlogic", "s-expressions", "rust"], 53 | classifiers=[ 54 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers for all 55 | # available setup classifiers 56 | "Development Status :: 5 - Production/Stable", 57 | "Intended Audience :: Developers", 58 | "License :: OSI Approved :: MIT License", 59 | "Natural Language :: English", 60 | "Operating System :: POSIX :: Linux", 61 | "Operating System :: MacOS :: MacOS X", 62 | "Operating System :: Microsoft :: Windows", 63 | "Programming Language :: Python", 64 | "Programming Language :: Python :: 3 :: Only", 65 | "Programming Language :: Python :: 3.7", 66 | "Programming Language :: Python :: 3.8", 67 | "Programming Language :: Python :: 3.9", 68 | "Programming Language :: Python :: 3.10", 69 | "Programming Language :: Python :: 3.11", 70 | "Programming Language :: Python :: 3.12", 71 | "Programming Language :: Rust", 72 | # 'Programming Language :: Python :: Implementation :: PyPy', 73 | ], 74 | rust_extensions=[ 75 | RustExtension( 76 | # Python package name before the dot, name of C extension to 77 | # stick inside of it after the dot. 78 | "jsonlogic_rs.jsonlogic", 79 | "Cargo.toml", 80 | features=["python"], 81 | binding=Binding.RustCPython, 82 | ) 83 | ], 84 | packages=["jsonlogic_rs"], 85 | package_dir={"": "py"}, 86 | include_package_data=True, 87 | setup_requires=SETUP_REQUIRES, 88 | # rust extensions are not zip safe, just like C-extensions. 89 | zip_safe=False, 90 | ) 91 | -------------------------------------------------------------------------------- /src/bin.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::io::Read; 3 | 4 | use anyhow::{Context, Result}; 5 | use clap::{App, Arg}; 6 | use serde_json; 7 | use serde_json::Value; 8 | 9 | use jsonlogic_rs; 10 | 11 | fn configure_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { 12 | app.version(env!("CARGO_PKG_VERSION")) 13 | .author("Matthew Planchard ") 14 | .about( 15 | "Parse JSON data with a JsonLogic rule.\n\ 16 | \n\ 17 | When no or is -, read from stdin. 18 | \n\ 19 | The result is written to stdout as JSON, so multiple calls \n\ 20 | can be chained together if desired.", 21 | ) 22 | .arg( 23 | Arg::with_name("logic") 24 | .help("A JSON logic string") 25 | .required(true) 26 | .takes_value(true), 27 | ) 28 | .arg( 29 | Arg::with_name("data") 30 | .help("A string of JSON data to parse. May be provided as stdin.") 31 | .required(false) 32 | .takes_value(true), 33 | ) 34 | .after_help( 35 | r#"EXAMPLES: 36 | jsonlogic '{"===": [{"var": "a"}, "foo"]}' '{"a": "foo"}' 37 | jsonlogic '{"===": [1, 1]}' null 38 | echo '{"a": "foo"}' | jsonlogic '{"===": [{"var": "a"}, "foo"]}' 39 | 40 | Inspired by and conformant with the original JsonLogic (jsonlogic.com). 41 | 42 | Report bugs to github.com/Bestowinc/json-logic-rs."#, 43 | ) 44 | } 45 | 46 | fn main() -> Result<()> { 47 | let app = configure_args(App::new("jsonlogic")); 48 | let matches = app.get_matches(); 49 | 50 | let logic = matches.value_of("logic").expect("logic arg expected"); 51 | let json_logic: Value = 52 | serde_json::from_str(logic).context("Could not parse logic as JSON")?; 53 | 54 | // let mut data: String; 55 | let data_arg = matches.value_of("data").unwrap_or("-"); 56 | 57 | let mut data: String; 58 | if data_arg != "-" { 59 | data = data_arg.to_string(); 60 | } else { 61 | data = String::new(); 62 | io::stdin().lock().read_to_string(&mut data)?; 63 | } 64 | let json_data: Value = 65 | serde_json::from_str(&data).context("Could not parse data as JSON")?; 66 | 67 | let result = jsonlogic_rs::apply(&json_logic, &json_data) 68 | .context("Could not execute logic")?; 69 | 70 | println!("{}", result.to_string()); 71 | 72 | Ok(()) 73 | } 74 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | //! Error handling 2 | //! 3 | use serde_json::Value; 4 | use thiserror; 5 | 6 | use crate::op::NumParams; 7 | 8 | /// Public error enumeration 9 | #[derive(thiserror::Error, Debug)] 10 | pub enum Error { 11 | #[error("Invalid data - value: {value:?}, reason: {reason:?}")] 12 | InvalidData { value: Value, reason: String }, 13 | 14 | #[error("Invalid rule - operator: '{key:?}', reason: {reason:?}")] 15 | InvalidOperation { key: String, reason: String }, 16 | 17 | #[error("Invalid variable - '{value:?}', reason: {reason:?}")] 18 | InvalidVariable { value: Value, reason: String }, 19 | 20 | #[error("Invalid variable key - '{value:?}', reason: {reason:?}")] 21 | InvalidVariableKey { value: Value, reason: String }, 22 | 23 | #[error("Invalid argument for '{operation}' - '{value:?}', reason: {reason}")] 24 | InvalidArgument { 25 | value: Value, 26 | operation: String, 27 | reason: String, 28 | }, 29 | 30 | #[error("Invalid variable mapping - {0} is not an object.")] 31 | InvalidVarMap(Value), 32 | 33 | #[error("Encountered an unexpected error. Please raise an issue on GitHub and include the following error message: {0}")] 34 | UnexpectedError(String), 35 | 36 | #[error("Wrong argument count - expected: {expected:?}, actual: {actual:?}")] 37 | WrongArgumentCount { expected: NumParams, actual: usize }, 38 | } 39 | -------------------------------------------------------------------------------- /src/func.rs: -------------------------------------------------------------------------------- 1 | //! FUnctions 2 | 3 | use serde_json::Value; 4 | 5 | use crate::error::Error; 6 | 7 | /// A (potentially user-defined) function 8 | /// 9 | /// The simplest function definition looks like: 10 | /// 11 | /// ```jsonc 12 | /// { 13 | /// "def": [ // function definition operator 14 | /// "is_even", // function name 15 | /// [a], // function params 16 | /// // function expression 17 | /// { 18 | /// "===": [ 19 | /// {"%": [{"param": "a"}, 2]}, 20 | /// 0 21 | /// ] 22 | /// } 23 | /// ] 24 | /// } 25 | /// ``` 26 | /// 27 | /// Once defined, the above function can be used like: 28 | /// 29 | /// ```jsonc 30 | /// {"is_even": [5]} // false 31 | /// {"is_even": [2]} // true 32 | /// ``` 33 | /// 34 | /// Function expressions may use any of the standard operators or any 35 | /// previously defined functions. 36 | /// 37 | pub struct Function { 38 | name: String, 39 | params: Vec, 40 | expression: Value, 41 | } 42 | -------------------------------------------------------------------------------- /src/js_op.rs: -------------------------------------------------------------------------------- 1 | //! Implementations of JavaScript operators for JSON Values 2 | 3 | use serde_json::{Number, Value}; 4 | use std::f64; 5 | use std::str::FromStr; 6 | 7 | use crate::error::Error; 8 | 9 | // numeric characters according to parseFloat 10 | const NUMERICS: &'static [char] = &[ 11 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '.', '-', '+', 'e', 'E', 12 | ]; 13 | 14 | // TODOS: 15 | // - there are too many tests in docstrings 16 | // - the docstrings are too sarcastic about JS equality 17 | 18 | pub fn to_string(value: &Value) -> String { 19 | match value { 20 | Value::Object(_) => String::from("[object Object]"), 21 | Value::Bool(val) => val.to_string(), 22 | Value::Null => String::from("null"), 23 | Value::Number(val) => val.to_string(), 24 | Value::String(val) => String::from(val), 25 | Value::Array(val) => val 26 | .iter() 27 | .map(|i| match i { 28 | Value::Null => String::from(""), 29 | _ => to_string(i), 30 | }) 31 | .collect::>() 32 | .join(","), 33 | } 34 | } 35 | 36 | /// Implement something like OrdinaryToPrimitive() with a Number hint. 37 | /// 38 | /// If it's possible to return a numeric primitive, returns Some. 39 | /// Otherwise, return None. 40 | fn to_primitive_number(value: &Value) -> Option { 41 | match value { 42 | // .valueOf() returns the object itself, which is not a primitive 43 | Value::Object(_) => None, 44 | // .valueOf() returns the array itself 45 | Value::Array(_) => None, 46 | Value::Bool(val) => { 47 | if *val { 48 | Some(1.0) 49 | } else { 50 | Some(0.0) 51 | } 52 | } 53 | Value::Null => Some(0.0), 54 | Value::Number(val) => val.as_f64(), 55 | Value::String(_) => None, // already a primitive 56 | } 57 | } 58 | 59 | pub fn str_to_number>(string: S) -> Option { 60 | let s = string.as_ref(); 61 | if s == "" { 62 | Some(0.0) 63 | } else { 64 | f64::from_str(s).ok() 65 | } 66 | } 67 | 68 | enum Primitive { 69 | String(String), 70 | Number(f64), 71 | } 72 | 73 | #[allow(dead_code)] 74 | enum PrimitiveHint { 75 | String, 76 | Number, 77 | Default, 78 | } 79 | 80 | fn to_primitive(value: &Value, hint: PrimitiveHint) -> Primitive { 81 | match hint { 82 | PrimitiveHint::String => Primitive::String(to_string(value)), 83 | _ => to_primitive_number(value) 84 | .map(Primitive::Number) 85 | .unwrap_or(Primitive::String(to_string(value))), 86 | } 87 | } 88 | 89 | /// Do our best to convert something into a number. 90 | /// 91 | /// Should be pretty much equivalent to calling Number(value) in JS, 92 | /// returning None where that would return NaN. 93 | pub fn to_number(value: &Value) -> Option { 94 | match to_primitive(value, PrimitiveHint::Number) { 95 | Primitive::Number(num) => Some(num), 96 | Primitive::String(string) => str_to_number(string), 97 | } 98 | } 99 | 100 | /// Compare values in the JavaScript `==` style 101 | /// 102 | /// Implements the Abstract Equality Comparison algorithm (`==` in JS) 103 | /// as defined [here](https://www.ecma-international.org/ecma-262/5.1/#sec-11.9.3). 104 | /// 105 | /// ```rust 106 | /// use serde_json::json; 107 | /// use jsonlogic_rs::js_op::abstract_eq; 108 | /// 109 | /// assert!( 110 | /// abstract_eq( 111 | /// &json!(null), 112 | /// &json!(null), 113 | /// ) 114 | /// ); 115 | /// assert!( 116 | /// abstract_eq( 117 | /// &json!(1.0), 118 | /// &json!(1), 119 | /// ) 120 | /// ); 121 | /// assert!( 122 | /// abstract_eq( 123 | /// &json!("foo"), 124 | /// &json!("foo"), 125 | /// ) 126 | /// ); 127 | /// assert!( 128 | /// abstract_eq( 129 | /// &json!(true), 130 | /// &json!(true), 131 | /// ) 132 | /// ); 133 | /// assert!( 134 | /// abstract_eq( 135 | /// &json!("1"), 136 | /// &json!(1.0), 137 | /// ) 138 | /// ); 139 | /// assert!( 140 | /// abstract_eq( 141 | /// &json!(1.0), 142 | /// &json!("1"), 143 | /// ) 144 | /// ); 145 | /// assert!( 146 | /// abstract_eq( 147 | /// &json!(true), 148 | /// &json!("1"), 149 | /// ) 150 | /// ); 151 | /// assert!( 152 | /// abstract_eq( 153 | /// &json!(true), 154 | /// &json!(1.0), 155 | /// ) 156 | /// ); 157 | /// assert!( 158 | /// abstract_eq( 159 | /// &json!({}), 160 | /// &json!("[object Object]"), 161 | /// ) 162 | /// ); 163 | /// 164 | /// assert!( 165 | /// ! abstract_eq( 166 | /// &json!({}), 167 | /// &json!({}), 168 | /// ) 169 | /// ); 170 | /// assert!( 171 | /// ! abstract_eq( 172 | /// &json!([]), 173 | /// &json!([]), 174 | /// ) 175 | /// ); 176 | /// ``` 177 | pub fn abstract_eq(first: &Value, second: &Value) -> bool { 178 | // Follows the ECMA specification 2019:7.2.14 (Abstract Equality Comparison) 179 | match (first, second) { 180 | // 1. If Type(x) is the same as Type(y), then 181 | // a. If Type(x) is Undefined, return true. 182 | // - No need to handle this case, b/c undefined is not in JSON 183 | // b. If Type(x) is Null, return true. 184 | (Value::Null, Value::Null) => true, 185 | // c. If Type(x) is Number, then 186 | (Value::Number(x), Value::Number(y)) => { 187 | // i. If x is NaN, return false. 188 | // - we can ignore this case, b/c NaN is not in JSON 189 | // ii. If y is NaN, return false. 190 | // - same here 191 | // iii. If x is the same Number value as y, return true. 192 | x.as_f64() 193 | .map(|x_val| y.as_f64().map(|y_val| x_val == y_val).unwrap_or(false)) 194 | .unwrap_or(false) 195 | // x.as_f64() == y.as_f64() 196 | // iv. If x is +0 and y is −0, return true. 197 | // - with serde's Number, this is handled by the above 198 | // v. If x is −0 and y is +0, return true. 199 | // - same here 200 | // vi. Return false. 201 | // - done! 202 | } 203 | // d. If Type(x) is String, then return true if x and y are exactly 204 | // the same sequence of characters (same length and same characters 205 | // in corresponding positions). Otherwise, return false. 206 | (Value::String(x), Value::String(y)) => x == y, 207 | // e. If Type(x) is Boolean, return true if x and y are both true 208 | // or both false. Otherwise, return false. 209 | (Value::Bool(x), Value::Bool(y)) => x == y, 210 | // f. Return true if x and y refer to the same object. Otherwise, return false. 211 | // - not applicable to comparisons from JSON 212 | // 2. If x is null and y is undefined, return true. 213 | // - not applicable to JSON b/c there is no undefined 214 | // 3. If x is undefined and y is null, return true. 215 | // - not applicable to JSON b/c there is no undefined 216 | // 4. If Type(x) is Number and Type(y) is String, return the result of 217 | // the comparison x == ToNumber(y). 218 | (Value::Number(x), Value::String(y)) => { 219 | // the empty string is 0 220 | let y_res = str_to_number(y); 221 | y_res 222 | .map(|y_number| { 223 | x.as_f64() 224 | .map(|x_number| x_number == y_number) 225 | .unwrap_or(false) 226 | }) 227 | .unwrap_or(false) 228 | } 229 | // 5. If Type(x) is String and Type(y) is Number, return the result 230 | // of the comparison ToNumber(x) == y. 231 | (Value::String(x), Value::Number(y)) => { 232 | let x_res = str_to_number(x); 233 | x_res 234 | .map(|x_number| { 235 | y.as_f64() 236 | .map(|y_number| x_number == y_number) 237 | .unwrap_or(false) 238 | }) 239 | .unwrap_or(false) 240 | } 241 | // 6. If Type(x) is Boolean, return the result of the comparison ToNumber(x) == y. 242 | (Value::Bool(x), _) => match x { 243 | true => Number::from_f64(1 as f64) 244 | .map(|num| { 245 | let value = Value::Number(num); 246 | abstract_eq(&value, second) 247 | }) 248 | .unwrap_or(false), 249 | false => Number::from_f64(0 as f64) 250 | .map(|num| { 251 | let value = Value::Number(num); 252 | abstract_eq(&value, second) 253 | }) 254 | .unwrap_or(false), 255 | }, 256 | // 7. If Type(y) is Boolean, return the result of the comparison x == ToNumber(y). 257 | (_, Value::Bool(y)) => match y { 258 | true => Number::from_f64(1 as f64) 259 | .map(|num| { 260 | let value = Value::Number(num); 261 | abstract_eq(first, &value) 262 | }) 263 | .unwrap_or(false), 264 | false => Number::from_f64(0 as f64) 265 | .map(|num| { 266 | let value = Value::Number(num); 267 | abstract_eq(first, &value) 268 | }) 269 | .unwrap_or(false), 270 | }, 271 | // 8. If Type(x) is either String, Number, or Symbol and Type(y) is 272 | // Object, return the result of the comparison x == ToPrimitive(y). 273 | // NB: the only type of Objects we get in JSON are regular old arrays 274 | // and regular old objects. ToPrimitive on the former yields a 275 | // stringification of its values, stuck together with commands, 276 | // but with no brackets on the outside. ToPrimitive on the later 277 | // is just always [object Object]. 278 | (Value::String(_), Value::Array(_)) | (Value::Number(_), Value::Array(_)) => { 279 | abstract_eq(first, &Value::String(to_string(second))) 280 | } 281 | (Value::String(_), Value::Object(_)) | (Value::Number(_), Value::Object(_)) => { 282 | abstract_eq(first, &Value::String(to_string(second))) 283 | } 284 | // 9. If Type(x) is Object and Type(y) is either String, Number, or 285 | // Symbol, return the result of the comparison ToPrimitive(x) == y. 286 | (Value::Object(_), Value::String(_)) | (Value::Object(_), Value::Number(_)) => { 287 | abstract_eq(&Value::String(to_string(first)), second) 288 | } 289 | (Value::Array(_), Value::String(_)) | (Value::Array(_), Value::Number(_)) => { 290 | abstract_eq(&Value::String(to_string(first)), second) 291 | } 292 | _ => false, 293 | } 294 | } 295 | 296 | /// Perform JS-style strict equality 297 | /// 298 | /// Items are strictly equal if: 299 | /// - They are the same non-primitive object 300 | /// - They are a primitive object of the same type with the same value 301 | /// 302 | /// ```rust 303 | /// use serde_json::json; 304 | /// use jsonlogic_rs::js_op::strict_eq; 305 | /// 306 | /// // References of the same type and value are strictly equal 307 | /// assert!(strict_eq(&json!(1), &json!(1))); 308 | /// assert!(strict_eq(&json!(false), &json!(false))); 309 | /// assert!(strict_eq(&json!("foo"), &json!("foo"))); 310 | /// 311 | /// // "Abstract" type conversion is not performed for strict equality 312 | /// assert!(!strict_eq(&json!(0), &json!(false))); 313 | /// assert!(!strict_eq(&json!(""), &json!(0))); 314 | /// 315 | /// // Objects only compare equal if they are the same reference 316 | /// assert!(!strict_eq(&json!([]), &json!([]))); 317 | /// assert!(!strict_eq(&json!({}), &json!({}))); 318 | /// 319 | /// let arr = json!([]); 320 | /// let obj = json!({}); 321 | /// assert!(strict_eq(&arr, &arr)); 322 | /// assert!(strict_eq(&obj, &obj)); 323 | /// ``` 324 | /// 325 | pub fn strict_eq(first: &Value, second: &Value) -> bool { 326 | if std::ptr::eq(first, second) { 327 | return true; 328 | }; 329 | match (first, second) { 330 | (Value::Null, Value::Null) => true, 331 | (Value::Bool(x), Value::Bool(y)) => x == y, 332 | (Value::Number(x), Value::Number(y)) => x 333 | .as_f64() 334 | .and_then(|x_val| y.as_f64().map(|y_val| x_val == y_val)) 335 | .unwrap_or(false), 336 | (Value::String(x), Value::String(y)) => x == y, 337 | _ => false, 338 | } 339 | } 340 | 341 | pub fn strict_ne(first: &Value, second: &Value) -> bool { 342 | !strict_eq(first, second) 343 | } 344 | 345 | /// Perform JS-style abstract less-than 346 | /// 347 | /// 348 | /// ```rust 349 | /// use serde_json::json; 350 | /// use jsonlogic_rs::js_op::abstract_lt; 351 | /// 352 | /// assert_eq!(abstract_lt(&json!(-1), &json!(0)), true); 353 | /// assert_eq!(abstract_lt(&json!("-1"), &json!(0)), true); 354 | /// assert_eq!(abstract_lt(&json!(0), &json!(1)), true); 355 | /// assert_eq!(abstract_lt(&json!(0), &json!("1")), true); 356 | /// assert_eq!(abstract_lt(&json!(0), &json!("a")), false); 357 | /// ``` 358 | pub fn abstract_lt(first: &Value, second: &Value) -> bool { 359 | match ( 360 | to_primitive(first, PrimitiveHint::Number), 361 | to_primitive(second, PrimitiveHint::Number), 362 | ) { 363 | (Primitive::String(f), Primitive::String(s)) => f < s, 364 | (Primitive::Number(f), Primitive::Number(s)) => f < s, 365 | (Primitive::String(f), Primitive::Number(s)) => { 366 | if let Some(f) = str_to_number(f) { 367 | f < s 368 | } else { 369 | false 370 | } 371 | } 372 | (Primitive::Number(f), Primitive::String(s)) => { 373 | if let Some(s) = str_to_number(s) { 374 | f < s 375 | } else { 376 | false 377 | } 378 | } 379 | } 380 | } 381 | 382 | /// JS-style abstract gt 383 | /// 384 | /// ```rust 385 | /// use serde_json::json; 386 | /// use jsonlogic_rs::js_op::abstract_gt; 387 | /// 388 | /// assert_eq!(abstract_gt(&json!(0), &json!(-1)), true); 389 | /// assert_eq!(abstract_gt(&json!(0), &json!("-1")), true); 390 | /// assert_eq!(abstract_gt(&json!(1), &json!(0)), true); 391 | /// assert_eq!(abstract_gt(&json!("1"), &json!(0)), true); 392 | /// ``` 393 | pub fn abstract_gt(first: &Value, second: &Value) -> bool { 394 | match ( 395 | to_primitive(first, PrimitiveHint::Number), 396 | to_primitive(second, PrimitiveHint::Number), 397 | ) { 398 | (Primitive::String(f), Primitive::String(s)) => f > s, 399 | (Primitive::Number(f), Primitive::Number(s)) => f > s, 400 | (Primitive::String(f), Primitive::Number(s)) => { 401 | if let Some(f) = str_to_number(f) { 402 | f > s 403 | } else { 404 | false 405 | } 406 | } 407 | (Primitive::Number(f), Primitive::String(s)) => { 408 | if let Some(s) = str_to_number(s) { 409 | f > s 410 | } else { 411 | false 412 | } 413 | } 414 | } 415 | } 416 | 417 | /// Abstract inequality 418 | pub fn abstract_ne(first: &Value, second: &Value) -> bool { 419 | !abstract_eq(first, second) 420 | } 421 | 422 | /// Provide abstract <= comparisons 423 | pub fn abstract_lte(first: &Value, second: &Value) -> bool { 424 | abstract_lt(first, second) || abstract_eq(first, second) 425 | } 426 | 427 | /// Provide abstract >= comparisons 428 | pub fn abstract_gte(first: &Value, second: &Value) -> bool { 429 | abstract_gt(first, second) || abstract_eq(first, second) 430 | } 431 | 432 | /// Get the max of an array of values, performing abstract type conversion 433 | pub fn abstract_max(items: &Vec<&Value>) -> Result { 434 | items 435 | .into_iter() 436 | .map(|v| { 437 | to_number(v).ok_or_else(|| Error::InvalidArgument { 438 | value: (*v).clone(), 439 | operation: "max".into(), 440 | reason: "Could not convert value to number".into(), 441 | }) 442 | }) 443 | .fold(Ok(f64::NEG_INFINITY), |acc, cur| { 444 | let max = acc?; 445 | match cur { 446 | Ok(num) => { 447 | if num > max { 448 | Ok(num) 449 | } else { 450 | Ok(max) 451 | } 452 | } 453 | _ => cur, 454 | } 455 | }) 456 | } 457 | 458 | /// Get the max of an array of values, performing abstract type conversion 459 | pub fn abstract_min(items: &Vec<&Value>) -> Result { 460 | items 461 | .into_iter() 462 | .map(|v| { 463 | to_number(v).ok_or_else(|| Error::InvalidArgument { 464 | value: (*v).clone(), 465 | operation: "max".into(), 466 | reason: "Could not convert value to number".into(), 467 | }) 468 | }) 469 | .fold(Ok(f64::INFINITY), |acc, cur| { 470 | let min = acc?; 471 | match cur { 472 | Ok(num) => { 473 | if num < min { 474 | Ok(num) 475 | } else { 476 | Ok(min) 477 | } 478 | } 479 | _ => cur, 480 | } 481 | }) 482 | } 483 | 484 | /// Do plus 485 | pub fn abstract_plus(first: &Value, second: &Value) -> Value { 486 | let first_num = to_primitive_number(first); 487 | let second_num = to_primitive_number(second); 488 | 489 | match (first_num, second_num) { 490 | (Some(f), Some(s)) => { 491 | return Value::Number(Number::from_f64(f + s).unwrap()); 492 | } 493 | _ => {} 494 | }; 495 | 496 | let first_string = to_string(first); 497 | let second_string = to_string(second); 498 | 499 | Value::String(first_string.chars().chain(second_string.chars()).collect()) 500 | } 501 | 502 | /// Add values, parsing to floats first. 503 | /// 504 | /// The JSONLogic reference implementation uses the JS `parseFloat` operation 505 | /// on the parameters, which behaves quite differently from the normal JS 506 | /// numeric conversion with `Number(val)`. While the latter uses the 507 | /// `toPrimitive` method on the base object Prototype, the former first 508 | /// converts any incoming value to a string, and then tries to parse it 509 | /// as a float. The upshot is that things that normally parse fine into 510 | /// numbers in JS, like bools and null, convert to NaN, because you can't 511 | /// make "false" into a number. 512 | /// 513 | /// The JSONLogic reference implementation deals with any values that 514 | /// evaluate to NaN by returning null. We instead will return an error, 515 | /// the behavior for non-numeric inputs is not specified in the spec, 516 | /// and returning errors seems like a more reasonable course of action 517 | /// than returning null. 518 | pub fn parse_float_add(vals: &Vec<&Value>) -> Result { 519 | vals.into_iter() 520 | .map(|&v| { 521 | parse_float(v).ok_or_else(|| Error::InvalidArgument { 522 | value: v.clone(), 523 | operation: "+".into(), 524 | reason: "Argument could not be converted to a float".into(), 525 | }) 526 | }) 527 | .fold(Ok(0.0), |acc, cur| { 528 | let total = acc?; 529 | match cur { 530 | Ok(num) => Ok(total + num), 531 | _ => cur, 532 | } 533 | }) 534 | } 535 | 536 | /// Multiply values, parsing to floats first 537 | /// 538 | /// See notes for parse_float_add on how this differs from normal number 539 | /// conversion as is done for _other_ arithmetic operators in the reference 540 | /// implementation 541 | pub fn parse_float_mul(vals: &Vec<&Value>) -> Result { 542 | vals.into_iter() 543 | .map(|&v| { 544 | parse_float(v).ok_or_else(|| Error::InvalidArgument { 545 | value: v.clone(), 546 | operation: "*".into(), 547 | reason: "Argument could not be converted to a float".into(), 548 | }) 549 | }) 550 | .fold(Ok(1.0), |acc, cur| { 551 | let total = acc?; 552 | match cur { 553 | Ok(num) => Ok(total * num), 554 | _ => cur, 555 | } 556 | }) 557 | } 558 | 559 | /// Do minus 560 | pub fn abstract_minus(first: &Value, second: &Value) -> Result { 561 | let first_num = to_number(first); 562 | let second_num = to_number(second); 563 | 564 | if let None = first_num { 565 | return Err(Error::InvalidArgument { 566 | value: first.clone(), 567 | operation: "-".into(), 568 | reason: "Could not convert value to number.".into(), 569 | }); 570 | } 571 | if let None = second_num { 572 | return Err(Error::InvalidArgument { 573 | value: second.clone(), 574 | operation: "-".into(), 575 | reason: "Could not convert value to number.".into(), 576 | }); 577 | } 578 | 579 | Ok(first_num.unwrap() - second_num.unwrap()) 580 | } 581 | 582 | /// Do division 583 | pub fn abstract_div(first: &Value, second: &Value) -> Result { 584 | let first_num = to_number(first); 585 | let second_num = to_number(second); 586 | 587 | if let None = first_num { 588 | return Err(Error::InvalidArgument { 589 | value: first.clone(), 590 | operation: "/".into(), 591 | reason: "Could not convert value to number.".into(), 592 | }); 593 | } 594 | if let None = second_num { 595 | return Err(Error::InvalidArgument { 596 | value: second.clone(), 597 | operation: "/".into(), 598 | reason: "Could not convert value to number.".into(), 599 | }); 600 | } 601 | 602 | Ok(first_num.unwrap() / second_num.unwrap()) 603 | } 604 | 605 | /// Do modulo 606 | pub fn abstract_mod(first: &Value, second: &Value) -> Result { 607 | let first_num = to_number(first); 608 | let second_num = to_number(second); 609 | 610 | if let None = first_num { 611 | return Err(Error::InvalidArgument { 612 | value: first.clone(), 613 | operation: "%".into(), 614 | reason: "Could not convert value to number.".into(), 615 | }); 616 | } 617 | if let None = second_num { 618 | return Err(Error::InvalidArgument { 619 | value: second.clone(), 620 | operation: "%".into(), 621 | reason: "Could not convert value to number.".into(), 622 | }); 623 | } 624 | 625 | Ok(first_num.unwrap() % second_num.unwrap()) 626 | } 627 | 628 | /// Attempt to convert a value to a negative number 629 | pub fn to_negative(val: &Value) -> Result { 630 | to_number(val) 631 | .map(|v| -1.0 * v) 632 | .ok_or_else(|| Error::InvalidArgument { 633 | value: val.clone(), 634 | operation: "to_negative".into(), 635 | reason: "Could not convert value to a number".into(), 636 | }) 637 | } 638 | 639 | /// Try to parse a string as a float, javascript style 640 | /// 641 | /// Strip whitespace, accumulate any potentially numeric characters at the 642 | /// start of the string and try to convert them into a float. We don't 643 | /// quite follow the spec exactly: we don't deal with infinity 644 | /// and NaN. That is okay, because this is only used in a context dealing 645 | /// with JSON values, which can't be Infinity or NaN. 646 | fn parse_float_string(val: &String) -> Option { 647 | let (mut leading_numerics, _, _) = val.trim().chars().fold( 648 | (Vec::new(), false, false), 649 | |(mut acc, broke, saw_decimal), c| { 650 | if broke { 651 | // if we hit a nonnumeric last iter, just return what we've got 652 | (acc, broke, saw_decimal) 653 | } else if NUMERICS.contains(&c) { 654 | let is_decimal = c == '.'; 655 | if saw_decimal && is_decimal { 656 | // if we're a decimal and we've seen one before, break 657 | (acc, true, is_decimal) 658 | } else { 659 | // if we're a numeric, stick it on the acc 660 | acc.push(c); 661 | (acc, broke, saw_decimal || is_decimal) 662 | } 663 | } else { 664 | // return the acc as is and let 'em know we hit a nonnumeric 665 | (acc, true, saw_decimal) 666 | } 667 | }, 668 | ); 669 | // don't bother collecting into a string if we don't need to 670 | if leading_numerics.len() == 0 { 671 | return None; 672 | }; 673 | if let Some('e') | Some('E') = leading_numerics.last() { 674 | // If the last character is an 'e' or an `E`, remove it, to match 675 | // edge case where JS ignores a trailing `e` rather than treating it 676 | // as bad exponential notation, e.g. JS treats 1e as just 1. 677 | leading_numerics.pop(); 678 | } 679 | 680 | // collect into a string, try to parse as a float, return an option 681 | leading_numerics 682 | .iter() 683 | .collect::() 684 | .parse::() 685 | .ok() 686 | } 687 | 688 | /// Attempt to parse a value into a float. 689 | /// 690 | /// The implementation should match https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseFloat 691 | /// as closely as is reasonable. 692 | pub fn parse_float(val: &Value) -> Option { 693 | match val { 694 | Value::Number(num) => num.as_f64(), 695 | Value::String(string) => parse_float_string(string), 696 | _ => parse_float(&Value::String(to_string(&val))), 697 | } 698 | } 699 | 700 | // ===================================================================== 701 | // Unit Tests 702 | // ===================================================================== 703 | 704 | #[cfg(test)] 705 | mod abstract_operations { 706 | 707 | use super::*; 708 | use serde_json::json; 709 | 710 | fn equal_values() -> Vec<(Value, Value)> { 711 | vec![ 712 | (json!(null), json!(null)), 713 | (json!(1), json!(1)), 714 | (json!(1), json!(1.0)), 715 | (json!(1.0), json!(1)), 716 | (json!(0), json!(-0)), 717 | (json!(-0), json!(0)), 718 | (json!("foo"), json!("foo")), 719 | (json!(""), json!("")), 720 | (json!(true), json!(true)), 721 | (json!(false), json!(false)), 722 | (json!(1), json!("1")), 723 | (json!(1), json!("1.0")), 724 | (json!(1.0), json!("1.0")), 725 | (json!(1.0), json!("1")), 726 | (json!(0), json!("")), 727 | (json!(0), json!("0")), 728 | (json!(0), json!("-0")), 729 | (json!(0), json!("+0")), 730 | (json!(-1), json!("-1")), 731 | (json!(-1.0), json!("-1")), 732 | (json!(true), json!(1)), 733 | (json!(true), json!("1")), 734 | (json!(true), json!("1.0")), 735 | (json!(true), json!([1])), 736 | (json!(true), json!(["1"])), 737 | (json!(false), json!(0)), 738 | (json!(false), json!([])), 739 | (json!(false), json!([0])), 740 | (json!(false), json!("")), 741 | (json!(false), json!("0")), 742 | (json!("[object Object]"), json!({})), 743 | (json!("[object Object]"), json!({"a": "a"})), 744 | (json!(""), json!([])), 745 | (json!(""), json!([null])), 746 | (json!(","), json!([null, null])), 747 | (json!("1,2"), json!([1, 2])), 748 | (json!("a,b"), json!(["a", "b"])), 749 | (json!(0), json!([])), 750 | (json!(false), json!([])), 751 | (json!(true), json!([1])), 752 | (json!([]), json!("")), 753 | (json!([null]), json!("")), 754 | (json!([null, null]), json!(",")), 755 | (json!([1, 2]), json!("1,2")), 756 | (json!(["a", "b"]), json!("a,b")), 757 | (json!([]), json!(0)), 758 | (json!([0]), json!(0)), 759 | (json!([]), json!(false)), 760 | (json!([0]), json!(false)), 761 | (json!([1]), json!(true)), 762 | ] 763 | } 764 | 765 | fn lt_values() -> Vec<(Value, Value)> { 766 | vec![ 767 | (json!(-1), json!(0)), 768 | (json!("-1"), json!(0)), 769 | (json!(0), json!(1)), 770 | (json!(0), json!("1")), 771 | (json!("foo"), json!("foos")), 772 | (json!(""), json!("a")), 773 | (json!(""), json!([1])), 774 | (json!(""), json!([1, 2])), 775 | (json!(""), json!("1")), 776 | (json!(""), json!({})), 777 | (json!(""), json!({"a": 1})), 778 | (json!(false), json!(true)), 779 | (json!(false), json!(1)), 780 | (json!(false), json!("1")), 781 | (json!(false), json!([1])), 782 | (json!(null), json!(1)), 783 | (json!(null), json!(true)), 784 | (json!(null), json!("1")), 785 | (json!([]), json!([1])), 786 | (json!([]), json!([1, 2])), 787 | (json!(0), json!([1])), 788 | (json!("0"), json!({})), 789 | (json!("0"), json!({"a": 1})), 790 | (json!("0"), json!([1, 2])), 791 | ] 792 | } 793 | 794 | fn gt_values() -> Vec<(Value, Value)> { 795 | vec![ 796 | (json!(0), json!(-1)), 797 | (json!(0), json!("-1")), 798 | (json!(1), json!(0)), 799 | (json!("1"), json!(0)), 800 | (json!("foos"), json!("foo")), 801 | (json!("a"), json!("")), 802 | (json!([1]), json!("")), 803 | (json!("1"), json!("")), 804 | (json!("1"), json!("0")), 805 | (json!(true), json!(false)), 806 | (json!(1), json!(false)), 807 | (json!("1"), json!(false)), 808 | (json!([1]), json!(false)), 809 | (json!(1), json!(null)), 810 | (json!(true), json!(null)), 811 | (json!("1"), json!(null)), 812 | (json!([1]), json!([])), 813 | (json!([1, 2]), json!([])), 814 | ] 815 | } 816 | 817 | fn ne_values() -> Vec<(Value, Value)> { 818 | vec![ 819 | (json!([]), json!([])), 820 | (json!([1]), json!([1])), 821 | (json!([1, 1]), json!([1, 1])), 822 | (json!({}), json!({})), 823 | (json!({"a": 1}), json!({"a": 1})), 824 | (json!([]), json!({})), 825 | (json!(0), json!(1)), 826 | (json!("a"), json!("b")), 827 | (json!(true), json!(false)), 828 | (json!(true), json!([0])), 829 | (json!(1.0), json!(1.1)), 830 | (json!(null), json!(0)), 831 | (json!(null), json!("")), 832 | (json!(null), json!(false)), 833 | (json!(null), json!(true)), 834 | ] 835 | } 836 | 837 | /// Values that do not compare true for anything other than ne. 838 | fn not_gt_not_lt_not_eq() -> Vec<(Value, Value)> { 839 | vec![ 840 | (json!(null), json!("")), 841 | (json!(null), json!("a")), 842 | (json!(0), json!("a")), 843 | (json!(0), json!([1, 2])), 844 | (json!([]), json!([])), 845 | (json!([1]), json!([1])), 846 | (json!([1, 2]), json!([1, 2])), 847 | (json!({}), json!({})), 848 | (json!(false), json!({})), 849 | (json!(true), json!({})), 850 | (json!(false), json!([1, 2])), 851 | (json!(true), json!([1, 2])), 852 | ] 853 | } 854 | 855 | fn plus_cases() -> Vec<(Value, Value, Value)> { 856 | vec![ 857 | (json!(1), json!(1), json!(2.0)), 858 | (json!(1), json!(true), json!(2.0)), 859 | (json!(true), json!(true), json!(2.0)), 860 | (json!(1), json!(false), json!(1.0)), 861 | (json!(false), json!(false), json!(0.0)), 862 | (json!(1), json!(null), json!(1.0)), 863 | (json!(null), json!(null), json!(0.0)), 864 | (json!(1), json!("1"), json!("11")), 865 | (json!(1), json!([1]), json!("11")), 866 | (json!(1), json!([1, 2]), json!("11,2")), 867 | (json!(1), json!([1, null, 3]), json!("11,,3")), 868 | (json!(1), json!({}), json!("1[object Object]")), 869 | ] 870 | } 871 | 872 | #[test] 873 | fn test_to_string_obj() { 874 | assert_eq!(&to_string(&json!({})), "[object Object]"); 875 | assert_eq!(&to_string(&json!({"a": "b"})), "[object Object]"); 876 | } 877 | 878 | #[test] 879 | fn test_to_string_array() { 880 | assert_eq!(&to_string(&json!([])), ""); 881 | assert_eq!(&to_string(&json!([1, 2, 3])), "1,2,3"); 882 | assert_eq!(&to_string(&json!([1, [2, 3], 4])), "1,2,3,4"); 883 | assert_eq!(&to_string(&json!([1, {}, 2])), "1,[object Object],2"); 884 | assert_eq!(&to_string(&json!(["a", "b"])), "a,b"); 885 | assert_eq!(&to_string(&json!([null])), ""); 886 | assert_eq!(&to_string(&json!([null, 1, 2, null])), ",1,2,"); 887 | assert_eq!(&to_string(&json!([true, false])), "true,false"); 888 | } 889 | 890 | #[test] 891 | fn test_to_string_null() { 892 | assert_eq!(&to_string(&json!(null)), "null"); 893 | } 894 | 895 | #[test] 896 | fn test_to_string_bool() { 897 | assert_eq!(&to_string(&json!(true)), "true"); 898 | assert_eq!(&to_string(&json!(false)), "false"); 899 | } 900 | 901 | #[test] 902 | fn test_to_string_number() { 903 | assert_eq!(&to_string(&json!(1.0)), "1.0"); 904 | assert_eq!(&to_string(&json!(1)), "1"); 905 | } 906 | 907 | #[test] 908 | fn test_abstract_eq() { 909 | equal_values().iter().for_each(|(first, second)| { 910 | println!("{:?}-{:?}", &first, &second); 911 | assert!(abstract_eq(&first, &second), true); 912 | }) 913 | } 914 | 915 | #[test] 916 | fn test_abstract_ne() { 917 | ne_values().iter().for_each(|(first, second)| { 918 | println!("{:?}-{:?}", &first, &second); 919 | assert_eq!(abstract_ne(&first, &second), true); 920 | }) 921 | } 922 | 923 | #[test] 924 | fn test_abstract_lt() { 925 | lt_values().iter().for_each(|(first, second)| { 926 | println!("{:?}-{:?}", &first, &second); 927 | assert_eq!(abstract_lt(&first, &second), true); 928 | }) 929 | } 930 | 931 | #[test] 932 | fn test_abstract_gt() { 933 | gt_values().iter().for_each(|(first, second)| { 934 | println!("{:?}-{:?}", &first, &second); 935 | assert_eq!(abstract_gt(&first, &second), true); 936 | }) 937 | } 938 | 939 | #[test] 940 | fn test_eq_values_are_not_lt() { 941 | equal_values().iter().for_each(|(first, second)| { 942 | println!("{:?}-{:?}", &first, &second); 943 | assert_eq!(abstract_lt(&first, &second), false); 944 | }) 945 | } 946 | 947 | #[test] 948 | fn test_eq_values_are_not_gt() { 949 | equal_values().iter().for_each(|(first, second)| { 950 | println!("{:?}-{:?}", &first, &second); 951 | assert_eq!(abstract_gt(&first, &second), false); 952 | }) 953 | } 954 | 955 | #[test] 956 | fn test_eq_values_are_not_ne() { 957 | equal_values().iter().for_each(|(first, second)| { 958 | println!("{:?}-{:?}", &first, &second); 959 | assert_eq!(abstract_ne(&first, &second), false); 960 | }) 961 | } 962 | 963 | #[test] 964 | fn test_lt_values_are_not_eq() { 965 | lt_values().iter().for_each(|(first, second)| { 966 | println!("{:?}-{:?}", &first, &second); 967 | assert_eq!(abstract_eq(&first, &second), false); 968 | }) 969 | } 970 | 971 | #[test] 972 | fn test_lt_values_are_not_gt() { 973 | lt_values().iter().for_each(|(first, second)| { 974 | println!("{:?}-{:?}", &first, &second); 975 | assert_eq!(abstract_gt(&first, &second), false); 976 | }) 977 | } 978 | 979 | #[test] 980 | fn test_lt_values_are_ne() { 981 | lt_values().iter().for_each(|(first, second)| { 982 | println!("{:?}-{:?}", &first, &second); 983 | assert_eq!(abstract_ne(&first, &second), true); 984 | }) 985 | } 986 | 987 | #[test] 988 | fn test_gt_values_are_not_eq() { 989 | gt_values().iter().for_each(|(first, second)| { 990 | println!("{:?}-{:?}", &first, &second); 991 | assert_eq!(abstract_eq(&first, &second), false); 992 | }) 993 | } 994 | 995 | #[test] 996 | fn test_gt_values_are_not_lt() { 997 | gt_values().iter().for_each(|(first, second)| { 998 | println!("{:?}-{:?}", &first, &second); 999 | assert_eq!(abstract_lt(&first, &second), false); 1000 | }) 1001 | } 1002 | 1003 | #[test] 1004 | fn test_gt_values_are_ne() { 1005 | gt_values().iter().for_each(|(first, second)| { 1006 | println!("{:?}-{:?}", &first, &second); 1007 | assert_eq!(abstract_ne(&first, &second), true); 1008 | }) 1009 | } 1010 | 1011 | #[test] 1012 | fn test_incomparable() { 1013 | not_gt_not_lt_not_eq().iter().for_each(|(first, second)| { 1014 | println!("{:?}-{:?}", &first, &second); 1015 | assert_eq!(abstract_lt(&first, &second), false); 1016 | assert_eq!(abstract_gt(&first, &second), false); 1017 | assert_eq!(abstract_eq(&first, &second), false); 1018 | }) 1019 | } 1020 | 1021 | // abstract_lte 1022 | 1023 | #[test] 1024 | fn test_lt_values_are_lte() { 1025 | lt_values().iter().for_each(|(first, second)| { 1026 | println!("{:?}-{:?}", &first, &second); 1027 | assert_eq!(abstract_lte(&first, &second), true); 1028 | }) 1029 | } 1030 | 1031 | #[test] 1032 | fn test_eq_values_are_lte() { 1033 | equal_values().iter().for_each(|(first, second)| { 1034 | println!("{:?}-{:?}", &first, &second); 1035 | assert_eq!(abstract_lte(&first, &second), true); 1036 | }) 1037 | } 1038 | 1039 | #[test] 1040 | fn test_gt_values_are_not_lte() { 1041 | gt_values().iter().for_each(|(first, second)| { 1042 | println!("{:?}-{:?}", &first, &second); 1043 | assert_eq!(abstract_lte(&first, &second), false); 1044 | }) 1045 | } 1046 | 1047 | // abstract_gte 1048 | 1049 | #[test] 1050 | fn test_gt_values_are_gte() { 1051 | gt_values().iter().for_each(|(first, second)| { 1052 | println!("{:?}-{:?}", &first, &second); 1053 | assert_eq!(abstract_gte(&first, &second), true); 1054 | }) 1055 | } 1056 | 1057 | #[test] 1058 | fn test_eq_values_are_gte() { 1059 | equal_values().iter().for_each(|(first, second)| { 1060 | println!("{:?}-{:?}", &first, &second); 1061 | assert_eq!(abstract_gte(&first, &second), true); 1062 | }) 1063 | } 1064 | 1065 | #[test] 1066 | fn test_lt_values_are_not_gte() { 1067 | lt_values().iter().for_each(|(first, second)| { 1068 | println!("{:?}-{:?}", &first, &second); 1069 | assert_eq!(abstract_gte(&first, &second), false); 1070 | }) 1071 | } 1072 | 1073 | #[test] 1074 | fn test_abstract_plus() { 1075 | plus_cases().iter().for_each(|(first, second, exp)| { 1076 | println!("{:?}-{:?}", &first, &second); 1077 | let result = abstract_plus(&first, &second); 1078 | match result { 1079 | Value::Number(ref i) => match exp { 1080 | Value::Number(j) => assert_eq!(i, j), 1081 | _ => assert!(false), 1082 | }, 1083 | Value::String(ref i) => match exp { 1084 | Value::String(j) => assert_eq!(i, j), 1085 | _ => assert!(false), 1086 | }, 1087 | _ => assert!(false), 1088 | } 1089 | }) 1090 | } 1091 | } 1092 | 1093 | #[cfg(test)] 1094 | mod test_abstract_max { 1095 | use super::*; 1096 | use serde_json::json; 1097 | 1098 | fn max_cases() -> Vec<(Vec, Result)> { 1099 | vec![ 1100 | (vec![json!(1), json!(2), json!(3)], Ok(3.0)), 1101 | (vec![json!("1"), json!(true), json!([1])], Ok(1.0)), 1102 | ( 1103 | vec![json!(""), json!(null), json!([]), json!(false)], 1104 | Ok(0.0), 1105 | ), 1106 | (vec![json!("foo")], Err(())), 1107 | (vec![], Ok(f64::NEG_INFINITY)), 1108 | ] 1109 | } 1110 | 1111 | #[test] 1112 | fn test_abstract_max() { 1113 | max_cases().into_iter().for_each(|(items, exp)| { 1114 | println!("Max: {:?}", items); 1115 | let res = abstract_max(&items.iter().collect()); 1116 | println!("Res: {:?}", res); 1117 | match exp { 1118 | Ok(exp) => assert_eq!(res.unwrap(), exp), 1119 | _ => { 1120 | res.unwrap_err(); 1121 | } 1122 | }; 1123 | }) 1124 | } 1125 | } 1126 | 1127 | #[cfg(test)] 1128 | mod test_abstract_min { 1129 | use super::*; 1130 | use serde_json::json; 1131 | 1132 | fn min_cases() -> Vec<(Vec, Result)> { 1133 | vec![ 1134 | (vec![json!(1), json!(2), json!(3)], Ok(1.0)), 1135 | (vec![json!("1"), json!(true), json!([1])], Ok(1.0)), 1136 | ( 1137 | vec![json!(""), json!(null), json!([]), json!(false)], 1138 | Ok(0.0), 1139 | ), 1140 | (vec![json!("foo")], Err(())), 1141 | (vec![], Ok(f64::INFINITY)), 1142 | ] 1143 | } 1144 | 1145 | #[test] 1146 | fn test_abstract_min() { 1147 | min_cases().into_iter().for_each(|(items, exp)| { 1148 | println!("Min: {:?}", items); 1149 | let res = abstract_min(&items.iter().collect()); 1150 | println!("Res: {:?}", res); 1151 | match exp { 1152 | Ok(exp) => assert_eq!(res.unwrap(), exp), 1153 | _ => { 1154 | res.unwrap_err(); 1155 | } 1156 | }; 1157 | }) 1158 | } 1159 | } 1160 | 1161 | #[cfg(test)] 1162 | mod test_abstract_minus { 1163 | use super::*; 1164 | use serde_json::json; 1165 | 1166 | fn minus_cases() -> Vec<(Value, Value, Result)> { 1167 | vec![ 1168 | (json!(5), json!(2), Ok(3.0)), 1169 | (json!(0), json!(2), Ok(-2.0)), 1170 | (json!("5"), json!(2), Ok(3.0)), 1171 | (json!(["5"]), json!(2), Ok(3.0)), 1172 | (json!(["5"]), json!(true), Ok(4.0)), 1173 | (json!("foo"), json!(true), Err(())), 1174 | ] 1175 | } 1176 | 1177 | #[test] 1178 | fn test_abstract_minus() { 1179 | minus_cases().into_iter().for_each(|(first, second, exp)| { 1180 | println!("Minus: {:?} - {:?}", first, second); 1181 | let res = abstract_minus(&first, &second); 1182 | println!("Res: {:?}", res); 1183 | match exp { 1184 | Ok(exp) => assert_eq!(res.unwrap(), exp), 1185 | _ => { 1186 | res.unwrap_err(); 1187 | } 1188 | } 1189 | }) 1190 | } 1191 | } 1192 | 1193 | #[cfg(test)] 1194 | mod test_strict { 1195 | 1196 | use super::*; 1197 | use serde_json::json; 1198 | 1199 | fn eq_values() -> Vec<(Value, Value)> { 1200 | vec![ 1201 | (json!(""), json!("")), 1202 | (json!("foo"), json!("foo")), 1203 | (json!(1), json!(1)), 1204 | (json!(1), json!(1.0)), 1205 | (json!(null), json!(null)), 1206 | (json!(true), json!(true)), 1207 | (json!(false), json!(false)), 1208 | ] 1209 | } 1210 | 1211 | fn ne_values() -> Vec<(Value, Value)> { 1212 | vec![ 1213 | (json!({}), json!({})), 1214 | (json!({"a": "a"}), json!({"a": "a"})), 1215 | (json!([]), json!([])), 1216 | (json!("foo"), json!("noop")), 1217 | (json!(1), json!(2)), 1218 | (json!(0), json!([])), 1219 | (json!(0), json!([0])), 1220 | (json!(false), json!(null)), 1221 | (json!(true), json!(false)), 1222 | (json!(false), json!(true)), 1223 | (json!(false), json!([])), 1224 | (json!(false), json!("")), 1225 | ] 1226 | } 1227 | 1228 | #[test] 1229 | fn test_strict_eq() { 1230 | eq_values().iter().for_each(|(first, second)| { 1231 | println!("{:?}-{:?}", &first, &second); 1232 | assert!(strict_eq(&first, &second)); 1233 | }); 1234 | ne_values().iter().for_each(|(first, second)| { 1235 | println!("{:?}-{:?}", &first, &second); 1236 | assert!(!strict_eq(&first, &second)); 1237 | }); 1238 | } 1239 | 1240 | #[test] 1241 | fn test_strict_eq_same_obj() { 1242 | let obj = json!({}); 1243 | assert!(strict_eq(&obj, &obj)) 1244 | } 1245 | 1246 | #[test] 1247 | fn test_strict_ne() { 1248 | ne_values().iter().for_each(|(first, second)| { 1249 | println!("{:?}-{:?}", &first, &second); 1250 | assert!(strict_ne(&first, &second)); 1251 | }); 1252 | eq_values().iter().for_each(|(first, second)| { 1253 | println!("{:?}-{:?}", &first, &second); 1254 | assert!(!strict_ne(&first, &second)); 1255 | }); 1256 | } 1257 | 1258 | #[test] 1259 | fn test_strict_ne_same_obj() { 1260 | let obj = json!({}); 1261 | assert!(!strict_ne(&obj, &obj)) 1262 | } 1263 | } 1264 | 1265 | #[cfg(test)] 1266 | mod test_parse_float { 1267 | use super::*; 1268 | use serde_json::json; 1269 | 1270 | fn cases() -> Vec<(Value, Option)> { 1271 | vec![ 1272 | (json!(1), Some(1.0)), 1273 | (json!(1.5), Some(1.5)), 1274 | (json!(-1.5), Some(-1.5)), 1275 | (json!("1"), Some(1.0)), 1276 | (json!("1e2"), Some(100.0)), 1277 | (json!("1E2"), Some(100.0)), 1278 | (json!("1.1e2"), Some(110.0)), 1279 | (json!("-1.1e2"), Some(-110.0)), 1280 | (json!("1e-2"), Some(0.01)), 1281 | (json!("1.0"), Some(1.0)), 1282 | (json!("1.1"), Some(1.1)), 1283 | (json!("1.1.1"), Some(1.1)), 1284 | (json!("1234abc"), Some(1234.0)), 1285 | (json!("1e"), Some(1.0)), 1286 | (json!("1E"), Some(1.0)), 1287 | (json!(false), None), 1288 | (json!(true), None), 1289 | (json!(null), None), 1290 | (json!("+5"), Some(5.0)), 1291 | (json!("-5"), Some(-5.0)), 1292 | (json!([]), None), 1293 | (json!([1]), Some(1.0)), 1294 | // this is weird, but correct. it converts to a string first 1295 | // "1,2" and then parses up to the first comma as a number 1296 | (json!([1, 2]), Some(1.0)), 1297 | (json!({}), None), 1298 | ] 1299 | } 1300 | 1301 | #[test] 1302 | fn test_parse_float() { 1303 | cases() 1304 | .into_iter() 1305 | .for_each(|(input, exp)| assert_eq!(parse_float(&input), exp)); 1306 | } 1307 | } 1308 | -------------------------------------------------------------------------------- /src/op/array.rs: -------------------------------------------------------------------------------- 1 | //! Array Operations 2 | //! 3 | //! Note that some array operations also operate on strings as arrays 4 | //! of characters. 5 | 6 | use serde_json::{Map, Value}; 7 | 8 | use crate::error::Error; 9 | use crate::op::logic; 10 | use crate::value::{Evaluated, Parsed}; 11 | 12 | /// Map an operation onto values 13 | pub fn map(data: &Value, args: &Vec<&Value>) -> Result { 14 | let (items, expression) = (args[0], args[1]); 15 | 16 | let _parsed = Parsed::from_value(items)?; 17 | let evaluated_items = _parsed.evaluate(data)?; 18 | 19 | let values: Vec<&Value> = match evaluated_items { 20 | Evaluated::New(Value::Array(ref vals)) => vals.iter().collect(), 21 | Evaluated::Raw(Value::Array(vals)) => vals.iter().collect(), 22 | // null is treated as an empty array in the reference tests, 23 | // for whatever reason 24 | Evaluated::New(Value::Null) => vec![], 25 | Evaluated::Raw(Value::Null) => vec![], 26 | _ => { 27 | return Err(Error::InvalidArgument { 28 | value: args[0].clone(), 29 | operation: "map".into(), 30 | reason: format!( 31 | "First argument to map must evaluate to an array. Got {:?}", 32 | evaluated_items 33 | ), 34 | }) 35 | } 36 | }; 37 | 38 | let parsed_expression = Parsed::from_value(expression)?; 39 | 40 | values 41 | .iter() 42 | .map(|v| parsed_expression.evaluate(v).map(Value::from)) 43 | .collect::, Error>>() 44 | .map(Value::Array) 45 | } 46 | 47 | /// Filter values by some predicate 48 | pub fn filter(data: &Value, args: &Vec<&Value>) -> Result { 49 | let (items, expression) = (args[0], args[1]); 50 | 51 | let _parsed = Parsed::from_value(items)?; 52 | let evaluated_items = _parsed.evaluate(data)?; 53 | 54 | let values: Vec = match evaluated_items { 55 | Evaluated::New(Value::Array(vals)) => vals, 56 | Evaluated::Raw(Value::Array(vals)) => { 57 | vals.into_iter().map(|v| v.clone()).collect() 58 | } 59 | // null is treated as an empty array in the reference tests, 60 | // for whatever reason 61 | Evaluated::New(Value::Null) => vec![], 62 | Evaluated::Raw(Value::Null) => vec![], 63 | _ => { 64 | return Err(Error::InvalidArgument { 65 | value: args[0].clone(), 66 | operation: "map".into(), 67 | reason: format!( 68 | "First argument to filter must evaluate to an array. Got {:?}", 69 | evaluated_items 70 | ), 71 | }) 72 | } 73 | }; 74 | 75 | let parsed_expression = Parsed::from_value(expression)?; 76 | 77 | let value_vec: Vec = Vec::with_capacity(values.len()); 78 | values 79 | .into_iter() 80 | .fold(Ok(value_vec), |acc, cur| { 81 | let mut filtered = acc?; 82 | let predicate = parsed_expression.evaluate(&cur)?; 83 | 84 | match logic::truthy_from_evaluated(&predicate) { 85 | true => { 86 | filtered.push(cur); 87 | Ok(filtered) 88 | } 89 | false => Ok(filtered), 90 | } 91 | }) 92 | .map(Value::Array) 93 | } 94 | 95 | /// Reduce values into a single result 96 | /// 97 | /// Note this differs from the reference implementation of jsonlogic 98 | /// (but not the spec), in that it evaluates the initializer as a 99 | /// jsonlogic expression rather than a raw value. 100 | pub fn reduce(data: &Value, args: &Vec<&Value>) -> Result { 101 | let (items, expression, initializer) = (args[0], args[1], args[2]); 102 | 103 | let _parsed_items = Parsed::from_value(items)?; 104 | let evaluated_items = _parsed_items.evaluate(data)?; 105 | 106 | let _parsed_initializer = Parsed::from_value(initializer)?; 107 | let evaluated_initializer = _parsed_initializer.evaluate(data)?; 108 | 109 | let values: Vec = match evaluated_items { 110 | Evaluated::New(Value::Array(vals)) => vals, 111 | Evaluated::Raw(Value::Array(vals)) => vals.iter().map(|v| v.clone()).collect(), 112 | // null is treated as an empty array in the reference tests, 113 | // for whatever reason 114 | Evaluated::New(Value::Null) => vec![], 115 | Evaluated::Raw(Value::Null) => vec![], 116 | _ => { 117 | return Err(Error::InvalidArgument { 118 | value: args[0].clone(), 119 | operation: "map".into(), 120 | reason: format!( 121 | "First argument to filter must evaluate to an array. Got {:?}", 122 | evaluated_items 123 | ), 124 | }) 125 | } 126 | }; 127 | 128 | let parsed_expression = Parsed::from_value(expression)?; 129 | 130 | values 131 | .into_iter() 132 | .fold(Ok(Value::from(evaluated_initializer)), |acc, cur| { 133 | let accumulator = acc?; 134 | let mut data = Map::with_capacity(2); 135 | data.insert("current".into(), cur); 136 | data.insert("accumulator".into(), accumulator); 137 | 138 | parsed_expression 139 | .evaluate(&Value::Object(data)) 140 | .map(Value::from) 141 | }) 142 | } 143 | 144 | /// Return whether all members of an array or string satisfy a predicate. 145 | /// 146 | /// The predicate does not need to return true or false explicitly. Its 147 | /// return is evaluated using the "truthy" definition specified in the 148 | /// jsonlogic spec. 149 | pub fn all(data: &Value, args: &Vec<&Value>) -> Result { 150 | let (first_arg, second_arg) = (args[0], args[1]); 151 | 152 | // The first argument must be an array of values or a string of chars 153 | // We won't bother parsing yet if the value is anything other than 154 | // an object, because we can short-circuit this function if any of 155 | // the items fail to match the predicate. However, we will parse 156 | // if it's an object, in case it evaluates to a string or array, which 157 | // we will then pass on 158 | 159 | let _new_item: Value; 160 | let potentially_evaled_first_arg = match first_arg { 161 | Value::Object(_) => { 162 | let parsed = Parsed::from_value(first_arg)?; 163 | let evaluated = parsed.evaluate(data)?; 164 | _new_item = evaluated.into(); 165 | &_new_item 166 | } 167 | _ => first_arg, 168 | }; 169 | 170 | let _new_arr: Vec; 171 | let items = match potentially_evaled_first_arg { 172 | Value::Array(items) => items, 173 | Value::String(string) => { 174 | _new_arr = string 175 | .chars() 176 | .into_iter() 177 | .map(|c| Value::String(c.to_string())) 178 | .collect(); 179 | &_new_arr 180 | } 181 | Value::Null => { 182 | _new_arr = Vec::new(); 183 | &_new_arr 184 | } 185 | _ => { 186 | return Err(Error::InvalidArgument { 187 | value: first_arg.clone(), 188 | operation: "all".into(), 189 | reason: format!( 190 | "First argument to all must evaluate to an array, string, or null, got {}", 191 | potentially_evaled_first_arg 192 | ), 193 | }) 194 | } 195 | }; 196 | 197 | // Special-case the empty array, since it for some reason is specified 198 | // to return false. 199 | if items.len() == 0 { 200 | return Ok(Value::Bool(false)); 201 | } 202 | 203 | // Note we _expect_ the predicate to be an operator, but it doesn't 204 | // necessarily have to be. all([1, 2, 3], 1) is a valid operation, 205 | // returning 1 for each of the items and thus evaluating to true. 206 | let predicate = Parsed::from_value(second_arg)?; 207 | 208 | let result = items.into_iter().fold(Ok(true), |acc, i| { 209 | acc.and_then(|res| { 210 | // "Short-circuit": return false if the previous eval was false 211 | if !res { 212 | return Ok(false); 213 | }; 214 | let _parsed_item = Parsed::from_value(i)?; 215 | // Evaluate each item as we go, in case we can short-circuit 216 | let evaluated_item = _parsed_item.evaluate(data)?; 217 | Ok(logic::truthy_from_evaluated( 218 | &predicate.evaluate(&evaluated_item.into())?, 219 | )) 220 | }) 221 | })?; 222 | 223 | Ok(Value::Bool(result)) 224 | } 225 | 226 | /// Return whether some members of an array or string satisfy a predicate. 227 | /// 228 | /// The predicate does not need to return true or false explicitly. Its 229 | /// return is evaluated using the "truthy" definition specified in the 230 | /// jsonlogic spec. 231 | pub fn some(data: &Value, args: &Vec<&Value>) -> Result { 232 | let (first_arg, second_arg) = (args[0], args[1]); 233 | 234 | // The first argument must be an array of values or a string of chars 235 | // We won't bother parsing yet if the value is anything other than 236 | // an object, because we can short-circuit this function if any of 237 | // the items fail to match the predicate. However, we will parse 238 | // if it's an object, in case it evaluates to a string or array, which 239 | // we will then pass on 240 | 241 | let _new_item: Value; 242 | let potentially_evaled_first_arg = match first_arg { 243 | Value::Object(_) => { 244 | let parsed = Parsed::from_value(first_arg)?; 245 | let evaluated = parsed.evaluate(data)?; 246 | _new_item = evaluated.into(); 247 | &_new_item 248 | } 249 | _ => first_arg, 250 | }; 251 | 252 | let _new_arr: Vec; 253 | let items = match potentially_evaled_first_arg { 254 | Value::Array(items) => items, 255 | Value::String(string) => { 256 | _new_arr = string 257 | .chars() 258 | .into_iter() 259 | .map(|c| Value::String(c.to_string())) 260 | .collect(); 261 | &_new_arr 262 | } 263 | Value::Null => { 264 | _new_arr = Vec::new(); 265 | &_new_arr 266 | } 267 | _ => { 268 | return Err(Error::InvalidArgument { 269 | value: first_arg.clone(), 270 | operation: "all".into(), 271 | reason: format!( 272 | "First argument must evaluate to an array, a string, or null, got {}", 273 | potentially_evaled_first_arg 274 | ), 275 | }) 276 | } 277 | }; 278 | 279 | // Special-case the empty array, since it for some reason is specified 280 | // to return false. 281 | if items.len() == 0 { 282 | return Ok(Value::Bool(false)); 283 | } 284 | 285 | // Note we _expect_ the predicate to be an operator, but it doesn't 286 | // necessarily have to be. all([1, 2, 3], 1) is a valid operation, 287 | // returning 1 for each of the items and thus evaluating to true. 288 | let predicate = Parsed::from_value(second_arg)?; 289 | 290 | let result = items.into_iter().fold(Ok(false), |acc, i| { 291 | acc.and_then(|res| { 292 | // "Short-circuit": return false if the previous eval was false 293 | if res { 294 | return Ok(true); 295 | }; 296 | let _parsed_item = Parsed::from_value(i)?; 297 | // Evaluate each item as we go, in case we can short-circuit 298 | let evaluated_item = _parsed_item.evaluate(data)?; 299 | Ok(logic::truthy_from_evaluated( 300 | &predicate.evaluate(&evaluated_item.into())?, 301 | )) 302 | }) 303 | })?; 304 | 305 | Ok(Value::Bool(result)) 306 | } 307 | 308 | /// Return whether no members of an array or string satisfy a predicate. 309 | /// 310 | /// The predicate does not need to return true or false explicitly. Its 311 | /// return is evaluated using the "truthy" definition specified in the 312 | /// jsonlogic spec. 313 | pub fn none(data: &Value, args: &Vec<&Value>) -> Result { 314 | some(data, args).and_then(|had_some| match had_some { 315 | Value::Bool(res) => Ok(Value::Bool(!res)), 316 | _ => Err(Error::UnexpectedError( 317 | "Unexpected return type from op_some".into(), 318 | )), 319 | }) 320 | } 321 | 322 | /// Merge one to n arrays, flattening them by one level. 323 | /// 324 | /// Values that are not arrays are (effectively) converted to arrays 325 | /// before flattening. 326 | pub fn merge(items: &Vec<&Value>) -> Result { 327 | let rv_vec: Vec = Vec::new(); 328 | Ok(Value::Array(items.into_iter().fold( 329 | rv_vec, 330 | |mut acc, i| { 331 | match i { 332 | Value::Array(i_vals) => { 333 | i_vals.into_iter().for_each(|val| acc.push((*val).clone())); 334 | } 335 | _ => acc.push((**i).clone()), 336 | }; 337 | acc 338 | }, 339 | ))) 340 | } 341 | 342 | /// Perform containment checks with "in" 343 | // TODO: make this a lazy operator, since we don't need to parse things 344 | // later on in the list if we find something that matches early. 345 | pub fn in_(items: &Vec<&Value>) -> Result { 346 | let needle = items[0]; 347 | let haystack = items[1]; 348 | 349 | match haystack { 350 | // Note: our containment check for array values is actually a bit 351 | // more robust than JS. This by default does array equality (e.g. 352 | // `[[1,2], [3,4]].contains([1,2]) == true`), as well as object 353 | // equality (e.g. `[{"a": "b"}].contains({"a": "b"}) == true`). 354 | // Given that anyone relying on this behavior in the existing jsonlogic 355 | // implementation is relying on broken, undefined behavior, it seems 356 | // okay to update that behavior to work in a more intuitive way. 357 | Value::Null => Ok(Value::Bool(false)), 358 | Value::Array(possibles) => Ok(Value::Bool(possibles.contains(needle))), 359 | Value::String(haystack_string) => { 360 | // Note: the reference implementation uses the regular old 361 | // String.prototype.indexOf() function to check for containment, 362 | // but that does JS type coercion, leading to crazy things like 363 | // `"foo[object Object]".indexOf({}) === 3`. Since the MDN docs 364 | // _explicitly_ say that the argument to indexOf should be a string, 365 | // we're going to take the same stance here, and throw an error 366 | // if the needle is a non-string for a haystack that's a string. 367 | let needle_string = 368 | match needle { 369 | Value::String(needle_string) => needle_string, 370 | _ => return Err(Error::InvalidArgument { 371 | value: needle.clone(), 372 | operation: "in".into(), 373 | reason: 374 | "If second argument is a string, first argument must also be a string." 375 | .into(), 376 | }), 377 | }; 378 | Ok(Value::Bool(haystack_string.contains(needle_string))) 379 | } 380 | _ => Err(Error::InvalidArgument { 381 | value: haystack.clone(), 382 | operation: "in".into(), 383 | reason: "Second argument must be an array or a string".into(), 384 | }), 385 | } 386 | } 387 | -------------------------------------------------------------------------------- /src/op/data.rs: -------------------------------------------------------------------------------- 1 | //! Data Operators 2 | 3 | use std::borrow::Cow; 4 | use std::convert::TryFrom; 5 | use std::convert::TryInto; 6 | 7 | use serde_json::Value; 8 | 9 | use crate::error::Error; 10 | use crate::value::{Evaluated, Parsed}; 11 | use crate::NULL; 12 | 13 | /// Valid types of variable keys 14 | enum KeyType<'a> { 15 | Null, 16 | String(Cow<'a, str>), 17 | Number(i64), 18 | } 19 | impl<'a> TryFrom for KeyType<'a> { 20 | type Error = Error; 21 | 22 | fn try_from(value: Value) -> Result { 23 | match value { 24 | Value::Null => Ok(Self::Null), 25 | Value::String(s) => Ok(Self::String(Cow::from(s))), 26 | Value::Number(n) => Ok(Self::Number(n.as_i64().ok_or_else(|| { 27 | Error::InvalidVariableKey { 28 | value: Value::Number(n), 29 | reason: "Numeric keys must be valid integers".into(), 30 | } 31 | })?)), 32 | _ => Err(Error::InvalidVariableKey { 33 | value: value.clone(), 34 | reason: "Variable keys must be strings, integers, or null".into(), 35 | }), 36 | } 37 | } 38 | } 39 | impl<'a> TryFrom<&'a Value> for KeyType<'a> { 40 | type Error = Error; 41 | 42 | fn try_from(value: &'a Value) -> Result { 43 | match value { 44 | Value::Null => Ok(Self::Null), 45 | Value::String(s) => Ok(Self::String(Cow::from(s))), 46 | Value::Number(n) => Ok(Self::Number(n.as_i64().ok_or_else(|| { 47 | Error::InvalidVariableKey { 48 | value: value.clone(), 49 | reason: "Numeric keys must be valid integers".into(), 50 | } 51 | })?)), 52 | _ => Err(Error::InvalidVariableKey { 53 | value: value.clone(), 54 | reason: "Variable keys must be strings, integers, or null".into(), 55 | }), 56 | } 57 | } 58 | } 59 | impl<'a> TryFrom> for KeyType<'a> { 60 | type Error = Error; 61 | 62 | fn try_from(value: Evaluated<'a>) -> Result { 63 | match value { 64 | Evaluated::Raw(v) => v.try_into(), 65 | Evaluated::New(v) => v.try_into(), 66 | } 67 | } 68 | } 69 | 70 | /// A get operation that supports negative indexes 71 | fn get(slice: &[T], idx: i64) -> Option<&T> { 72 | let vec_len = slice.len(); 73 | let usize_idx: usize = idx.abs().try_into().ok()?; 74 | 75 | let adjusted_idx = if idx >= 0 { 76 | usize_idx 77 | } else { 78 | vec_len.checked_sub(usize_idx)? 79 | }; 80 | 81 | slice.get(adjusted_idx) 82 | } 83 | 84 | /// Retrieve a variable from the data 85 | /// 86 | /// Note that the reference implementation does not support negative 87 | /// indexing for numeric values, but we do. 88 | pub fn var(data: &Value, args: &Vec<&Value>) -> Result { 89 | let arg_count = args.len(); 90 | if arg_count == 0 { 91 | return Ok(data.clone()); 92 | }; 93 | 94 | let key = args[0].try_into()?; 95 | let val = get_key(data, key); 96 | 97 | Ok(val.unwrap_or(if arg_count < 2 { 98 | NULL 99 | } else { 100 | let _parsed_default = Parsed::from_value(args[1])?; 101 | _parsed_default.evaluate(&data)?.into() 102 | })) 103 | } 104 | 105 | /// Check for keys that are missing from the data 106 | pub fn missing(data: &Value, args: &Vec<&Value>) -> Result { 107 | let mut missing_keys: Vec = Vec::new(); 108 | 109 | // This bit of insanity is because for some reason the reference 110 | // implementation is tested to do this, i.e. if missing is passed 111 | // multiple args and the first arg is an array, _that_ array is 112 | // treated as the only argument. 113 | let inner_vec: Vec<&Value>; 114 | let adjusted_args = if args.len() > 0 { 115 | match args[0] { 116 | Value::Array(vals) => { 117 | inner_vec = vals.iter().collect(); 118 | &inner_vec 119 | } 120 | _ => args, 121 | } 122 | } else { 123 | args 124 | }; 125 | 126 | adjusted_args.into_iter().fold(Ok(()), |had_error, arg| { 127 | had_error?; 128 | let key: KeyType = (*arg).try_into()?; 129 | match key { 130 | KeyType::Null => Ok(()), 131 | _ => { 132 | let val = get_key(data, key); 133 | if val.is_none() { 134 | missing_keys.push((*arg).clone()); 135 | }; 136 | Ok(()) 137 | } 138 | } 139 | })?; 140 | Ok(Value::Array(missing_keys)) 141 | } 142 | 143 | /// Check whether a minimum threshold of keys are present in the data 144 | /// 145 | /// Note that I think this function is confusingly named. `contains_at_least` 146 | /// might be better, or something like that. Regardless, it checks to see how 147 | /// many of the specified keys are present in the data. If there are equal 148 | /// to or more than the threshold value _present_ in the data, an empty 149 | /// array is returned. Otherwise, an array containing all missing keys 150 | /// is returned. 151 | pub fn missing_some(data: &Value, args: &Vec<&Value>) -> Result { 152 | let (threshold_arg, keys_arg) = (args[0], args[1]); 153 | 154 | let threshold = match threshold_arg { 155 | Value::Number(n) => n.as_u64(), 156 | _ => None, 157 | } 158 | .ok_or_else(|| Error::InvalidArgument { 159 | value: threshold_arg.clone(), 160 | operation: "missing_some".into(), 161 | reason: "missing_some threshold must be a valid, positive integer".into(), 162 | })?; 163 | 164 | let keys = match keys_arg { 165 | Value::Array(keys) => Ok(keys), 166 | _ => Err(Error::InvalidArgument { 167 | value: keys_arg.clone(), 168 | operation: "missig_some".into(), 169 | reason: "missing_some keys must be an array".into(), 170 | }), 171 | }?; 172 | 173 | let mut missing_keys: Vec = Vec::new(); 174 | let present_count = keys.into_iter().fold(Ok(0 as u64), |last, key| { 175 | // Don't bother evaluating once we've met the threshold. 176 | let prev_present_count = last?; 177 | if prev_present_count >= threshold { 178 | return Ok(prev_present_count); 179 | }; 180 | 181 | let parsed_key: KeyType = key.try_into()?; 182 | let current_present_count = match parsed_key { 183 | // In the reference implementation, I believe null actually is 184 | // buggy. Since usually, getting "null" as a var against the 185 | // data returns the whole data, "null" in a `missing_some` 186 | // list of keys _automatically_ counts as a present key, regardless 187 | // of what keys are in the data. This behavior is neither in the 188 | // specification nor the tests, so I'm going to SKIP null keys, 189 | // since they aren't valid Object or Array keys in JSON. 190 | KeyType::Null => prev_present_count, 191 | _ => { 192 | if get_key(data, parsed_key).is_none() && !missing_keys.contains(key) { 193 | missing_keys.push((*key).clone()); 194 | prev_present_count 195 | } else { 196 | prev_present_count + 1 197 | } 198 | } 199 | }; 200 | Ok(current_present_count) 201 | })?; 202 | 203 | let met_threshold = present_count >= threshold; 204 | 205 | if met_threshold { 206 | Ok(Value::Array(vec![])) 207 | } else { 208 | Ok(Value::Array(missing_keys)) 209 | } 210 | } 211 | 212 | fn get_key(data: &Value, key: KeyType) -> Option { 213 | match key { 214 | // If the key is null, we return the data, always, even if there 215 | // is a default parameter. 216 | KeyType::Null => return Some(data.clone()), 217 | KeyType::String(k) => get_str_key(data, k), 218 | KeyType::Number(i) => match data { 219 | Value::Object(_) => get_str_key(data, i.to_string()), 220 | Value::Array(arr) => get(arr, i).map(Value::clone), 221 | Value::String(s) => { 222 | let s_vec: Vec = s.chars().collect(); 223 | get(&s_vec, i).map(|c| c.to_string()).map(Value::String) 224 | } 225 | _ => None, 226 | }, 227 | } 228 | } 229 | 230 | pub fn split_with_escape(input: &str, delimiter: char) -> Vec { 231 | let mut result = Vec::new(); 232 | let mut slice = String::new(); 233 | let mut escape = false; 234 | 235 | for c in input.chars() { 236 | if escape { 237 | slice.push(c); 238 | escape = false; 239 | } else if c == '\\' { 240 | escape = true; 241 | } else if c == delimiter { 242 | result.push(slice.clone()); 243 | slice.clear(); 244 | } else { 245 | slice.push(c); 246 | } 247 | } 248 | 249 | if !slice.is_empty() { 250 | result.push(slice); 251 | } 252 | 253 | result 254 | } 255 | 256 | fn get_str_key>(data: &Value, key: K) -> Option { 257 | let k = key.as_ref(); 258 | if k == "" { 259 | return Some(data.clone()); 260 | }; 261 | match data { 262 | Value::Object(_) | Value::Array(_) | Value::String(_) => { 263 | // Exterior ref in case we need to make a new value in the match. 264 | split_with_escape(k, '.') 265 | .into_iter() 266 | .fold(Some(data.clone()), |acc, i| match acc? { 267 | // If the current value is an object, try to get the value 268 | Value::Object(map) => map.get(&i).map(Value::clone), 269 | // If the current value is an array, we need an integer 270 | // index. If integer conversion fails, return None. 271 | Value::Array(arr) => i 272 | .parse::() 273 | .ok() 274 | .and_then(|i| get(&arr, i)) 275 | .map(Value::clone), 276 | // Same deal if it's a string. 277 | Value::String(s) => { 278 | let s_chars: Vec = s.chars().collect(); 279 | i.parse::() 280 | .ok() 281 | .and_then(|i| get(&s_chars, i)) 282 | .map(|c| c.to_string()) 283 | .map(Value::String) 284 | } 285 | // This handles cases where we've got an un-indexable 286 | // type or similar. 287 | _ => None, 288 | }) 289 | } 290 | _ => None, 291 | } 292 | } 293 | 294 | #[cfg(test)] 295 | mod tests { 296 | use super::*; 297 | 298 | // All the tests cases have been discussed here: https://github.com/Bestowinc/json-logic-rs/pull/37 299 | fn cases() -> Vec<(&'static str, Vec<&'static str>)> { 300 | vec![ 301 | ("", vec![]), 302 | ("foo", vec!["foo"]), 303 | ("foo.bar", vec!["foo", "bar"]), 304 | (r#"foo\.bar"#, vec!["foo.bar"]), 305 | (r#"foo\.bar.biz"#, vec!["foo.bar", "biz"]), 306 | (r#"foo\\.bar"#, vec!["foo\\", "bar"]), 307 | (r#"foo\\.bar\.biz"#, vec!["foo\\", "bar.biz"]), 308 | (r#"foo\\\.bar"#, vec!["foo\\.bar"]), 309 | (r#"foo\\\.bar.biz"#, vec!["foo\\.bar", "biz"]), 310 | (r#"foo\\bar"#, vec!["foo\\bar"]), 311 | (r#"foo\\bar.biz"#, vec!["foo\\bar", "biz"]), 312 | (r#"foo\\bar\.biz"#, vec!["foo\\bar.biz"]), 313 | (r#"foo\\bar\\.biz"#, vec!["foo\\bar\\", "biz"]), 314 | ] 315 | } 316 | 317 | #[test] 318 | fn test_split_with_escape() { 319 | cases() 320 | .into_iter() 321 | .for_each(|(input, exp)| assert_eq!(split_with_escape(&input, '.'), exp)); 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /src/op/impure.rs: -------------------------------------------------------------------------------- 1 | //! Impure Operations 2 | 3 | use serde_json::Value; 4 | 5 | use crate::error::Error; 6 | 7 | /// Log the Operation's Value(s) 8 | /// 9 | /// The reference implementation ignores any arguments beyond the first, 10 | /// and the specification seems to indicate that the first argument is 11 | /// the only one considered, so we're doing the same. 12 | pub fn log(items: &Vec<&Value>) -> Result { 13 | println!("{}", items[0]); 14 | Ok(items[0].clone()) 15 | } 16 | -------------------------------------------------------------------------------- /src/op/logic.rs: -------------------------------------------------------------------------------- 1 | //! Boolean Logic Operations 2 | 3 | use serde_json::Value; 4 | 5 | use crate::error::Error; 6 | use crate::value::{Evaluated, Parsed}; 7 | use crate::NULL; 8 | 9 | /// Implement the "if" operator 10 | /// 11 | /// The base case works like: [condition, true, false] 12 | /// However, it can lso work like: 13 | /// [condition, true, condition2, true2, false2] 14 | /// for an if/elseif/else type of operation 15 | pub fn if_(data: &Value, args: &Vec<&Value>) -> Result { 16 | // Special case incorrect arguments. These are not defined in the 17 | // specification, but they are defined in the test cases. 18 | match args.len() { 19 | 0 => { 20 | return Ok(NULL); 21 | } 22 | // It's not totally clear to me why this would be the behavior, 23 | // rather than returning NULL regardless of how the single argument 24 | // evaluates, but this is I can gather is the expected behavior 25 | // from the tests. 26 | 1 => { 27 | let parsed = Parsed::from_value(args[0])?; 28 | let evaluated = parsed.evaluate(&data)?; 29 | return Ok(evaluated.into()); 30 | } 31 | _ => {} 32 | } 33 | 34 | args.into_iter() 35 | .enumerate() 36 | // Our accumulator is: 37 | // - last conditional evaluation value, 38 | // - whether that evaluation is truthy, 39 | // - whether we know we should return without further evaluation 40 | .fold(Ok((NULL, false, false)), |last_res, (i, val)| { 41 | let (last_eval, was_truthy, should_return) = last_res?; 42 | // We hit a final value already 43 | if should_return { 44 | Ok((last_eval, was_truthy, should_return)) 45 | } 46 | // Potential false-value, initial evaluation, or else-if clause 47 | else if i % 2 == 0 { 48 | let parsed = Parsed::from_value(val)?; 49 | let eval = parsed.evaluate(data)?; 50 | let is_truthy = match eval { 51 | Evaluated::New(ref v) => truthy(v), 52 | Evaluated::Raw(v) => truthy(v), 53 | }; 54 | // We're not sure we're the return value, so don't 55 | // force a return. 56 | Ok((eval.into(), is_truthy, false)) 57 | } 58 | // We're a possible true-value 59 | else { 60 | // If there was a previous evaluation and it was truthy, 61 | // return, and indicate we're a final value. 62 | if was_truthy { 63 | let parsed = Parsed::from_value(val)?; 64 | let t_eval = parsed.evaluate(data)?; 65 | Ok((Value::from(t_eval), true, true)) 66 | } else { 67 | // Return a null for the last eval to handle cases 68 | // where there is an incorrect number of arguments. 69 | Ok((NULL, was_truthy, should_return)) 70 | } 71 | } 72 | }) 73 | .map(|rv| rv.0) 74 | } 75 | 76 | /// Perform short-circuiting or evaluation 77 | pub fn or(data: &Value, args: &Vec<&Value>) -> Result { 78 | enum OrResult { 79 | Uninitialized, 80 | Truthy(Value), 81 | Current(Value), 82 | } 83 | 84 | let eval = 85 | args.into_iter() 86 | .fold(Ok(OrResult::Uninitialized), |last_res, current| { 87 | let last_eval = last_res?; 88 | 89 | // if we've found a truthy value, don't evaluate anything else 90 | if let OrResult::Truthy(_) = last_eval { 91 | return Ok(last_eval); 92 | } 93 | 94 | let parsed = Parsed::from_value(current)?; 95 | let evaluated = parsed.evaluate(data)?; 96 | 97 | if truthy_from_evaluated(&evaluated) { 98 | return Ok(OrResult::Truthy(evaluated.into())); 99 | } 100 | 101 | Ok(OrResult::Current(evaluated.into())) 102 | })?; 103 | 104 | match eval { 105 | OrResult::Truthy(v) => Ok(v), 106 | OrResult::Current(v) => Ok(v), 107 | _ => Err(Error::UnexpectedError( 108 | "Or operation had no values to operate on".into(), 109 | )), 110 | } 111 | } 112 | 113 | /// Perform short-circuiting and evaluation 114 | pub fn and(data: &Value, args: &Vec<&Value>) -> Result { 115 | enum AndResult { 116 | Uninitialized, 117 | Falsey(Value), 118 | Current(Value), 119 | } 120 | 121 | let eval = 122 | args.into_iter() 123 | .fold(Ok(AndResult::Uninitialized), |last_res, current| { 124 | let last_eval = last_res?; 125 | 126 | if let AndResult::Falsey(_) = last_eval { 127 | return Ok(last_eval); 128 | } 129 | 130 | let parsed = Parsed::from_value(current)?; 131 | let evaluated = parsed.evaluate(data)?; 132 | 133 | if !truthy_from_evaluated(&evaluated) { 134 | return Ok(AndResult::Falsey(evaluated.into())); 135 | } 136 | 137 | Ok(AndResult::Current(evaluated.into())) 138 | })?; 139 | 140 | match eval { 141 | AndResult::Falsey(v) => Ok(v), 142 | AndResult::Current(v) => Ok(v), 143 | _ => Err(Error::UnexpectedError( 144 | "And operation had no values to operate on".into(), 145 | )), 146 | } 147 | } 148 | 149 | pub fn truthy_from_evaluated(evaluated: &Evaluated) -> bool { 150 | match evaluated { 151 | Evaluated::New(ref v) => truthy(v), 152 | Evaluated::Raw(v) => truthy(v), 153 | } 154 | } 155 | 156 | /// Return whether a value is "truthy" by the JSONLogic spec 157 | /// 158 | /// The spec (http://jsonlogic.com/truthy) defines truthy values that 159 | /// diverge slightly from raw JavaScript. This ensures a matching 160 | /// interpretation. 161 | /// 162 | /// In general, the spec specifies that values are truthy or falsey 163 | /// depending on their containing something, e.g. non-zero integers, 164 | /// non-zero length strings, and non-zero length arrays are truthy. 165 | /// This does not apply to objects, which are always truthy. 166 | pub fn truthy(val: &Value) -> bool { 167 | match val { 168 | Value::Null => false, 169 | Value::Bool(v) => *v, 170 | Value::Number(v) => v 171 | .as_f64() 172 | .map(|v_num| if v_num == 0.0 { false } else { true }) 173 | .unwrap_or(false), 174 | Value::String(v) => { 175 | if v == "" { 176 | false 177 | } else { 178 | true 179 | } 180 | } 181 | Value::Array(v) => { 182 | if v.len() == 0 { 183 | false 184 | } else { 185 | true 186 | } 187 | } 188 | Value::Object(_) => true, 189 | } 190 | } 191 | 192 | #[cfg(test)] 193 | mod test_truthy { 194 | use super::*; 195 | use serde_json::json; 196 | 197 | #[test] 198 | fn test_truthy() { 199 | let trues = [ 200 | json!(true), 201 | json!([1]), 202 | json!([1, 2]), 203 | json!({}), 204 | json!({"a": 1}), 205 | json!(1), 206 | json!(-1), 207 | json!("foo"), 208 | ]; 209 | 210 | let falses = [json!(false), json!([]), json!(""), json!(0), json!(null)]; 211 | 212 | trues.iter().for_each(|v| assert!(truthy(&v))); 213 | falses.iter().for_each(|v| assert!(!truthy(&v))); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/op/mod.rs: -------------------------------------------------------------------------------- 1 | //! Operators 2 | //! 3 | //! This module contains the global operator map, which defines the available 4 | //! JsonLogic operations. Note that some "operations", notably data-related 5 | //! operations like "var" and "missing", are not included here, because they are 6 | //! implemented as parsers rather than operators. 7 | 8 | // TODO: it's possible that "missing", "var", et al. could be implemented 9 | // as operators. They were originally done differently because there wasn't 10 | // yet a LazyOperator concept. 11 | 12 | use phf::phf_map; 13 | use serde_json::{Map, Value}; 14 | use std::fmt; 15 | 16 | use crate::error::Error; 17 | use crate::value::to_number_value; 18 | use crate::value::{Evaluated, Parsed}; 19 | use crate::{js_op, Parser}; 20 | 21 | mod array; 22 | mod data; 23 | mod impure; 24 | mod logic; 25 | mod numeric; 26 | mod string; 27 | 28 | pub const OPERATOR_MAP: phf::Map<&'static str, Operator> = phf_map! { 29 | "==" => Operator { 30 | symbol: "==", 31 | operator: |items| Ok(Value::Bool(js_op::abstract_eq(items[0], items[1]))), 32 | num_params: NumParams::Exactly(2)}, 33 | "!=" => Operator { 34 | symbol: "!=", 35 | operator: |items| Ok(Value::Bool(js_op::abstract_ne(items[0], items[1]))), 36 | num_params: NumParams::Exactly(2)}, 37 | "===" => Operator { 38 | symbol: "===", 39 | operator: |items| Ok(Value::Bool(js_op::strict_eq(items[0], items[1]))), 40 | num_params: NumParams::Exactly(2)}, 41 | "!==" => Operator { 42 | symbol: "!==", 43 | operator: |items| Ok(Value::Bool(js_op::strict_ne(items[0], items[1]))), 44 | num_params: NumParams::Exactly(2)}, 45 | // Note: the ! and !! behavior conforms to the specification, but not the 46 | // reference implementation. The specification states: "Note: unary 47 | // operators can also take a single, non array argument." However, 48 | // if a non-unary array of arguments is passed to `!` or `!!` in the 49 | // reference implementation, it treats them as though they were a unary 50 | // array argument. I have chosen to conform to the spec because it leads 51 | // to less surprising behavior. I also think that the idea of taking 52 | // non-array unary arguments is ridiculous, particularly given that 53 | // the homepage of jsonlogic _also_ states that a "Virtue" of jsonlogic 54 | // is that it is "Consistent. `{"operator" : ["values" ... ]}` Always" 55 | "!" => Operator { 56 | symbol: "!", 57 | operator: |items| Ok(Value::Bool(!logic::truthy(items[0]))), 58 | num_params: NumParams::Unary, 59 | }, 60 | "!!" => Operator { 61 | symbol: "!!", 62 | operator: |items| Ok(Value::Bool(logic::truthy(items[0]))), 63 | num_params: NumParams::Unary, 64 | }, 65 | "<" => Operator { 66 | symbol: "<", 67 | operator: numeric::lt, 68 | num_params: NumParams::Variadic(2..4), 69 | }, 70 | "<=" => Operator { 71 | symbol: "<=", 72 | operator: numeric::lte, 73 | num_params: NumParams::Variadic(2..4), 74 | }, 75 | // Note: this is actually an _expansion_ on the specification and the 76 | // reference implementation. The spec states that < and <= can be used 77 | // for 2-3 arguments, with 3 arguments doing a "between" style test, 78 | // e.g. `1 < 2 < 3 == true`. However, this isn't explicitly supported 79 | // for > and >=, and the reference implementation simply ignores any 80 | // third value for these operators. This to me violates the principle 81 | // of least surprise, so we do support those operations. 82 | ">" => Operator { 83 | symbol: ">", 84 | operator: numeric::gt, 85 | num_params: NumParams::Variadic(2..4), 86 | }, 87 | ">=" => Operator { 88 | symbol: ">=", 89 | operator: numeric::gte, 90 | num_params: NumParams::Variadic(2..4), 91 | }, 92 | "+" => Operator { 93 | symbol: "+", 94 | operator: |items| js_op::parse_float_add(items).and_then(to_number_value), 95 | num_params: NumParams::Any, 96 | }, 97 | "-" => Operator { 98 | symbol: "-", 99 | operator: numeric::minus, 100 | num_params: NumParams::Variadic(1..3), 101 | }, 102 | "*" => Operator { 103 | symbol: "*", 104 | operator: |items| js_op::parse_float_mul(items).and_then(to_number_value), 105 | num_params: NumParams::AtLeast(1), 106 | }, 107 | "/" => Operator { 108 | symbol: "/", 109 | operator: |items| js_op::abstract_div(items[0], items[1]) 110 | .and_then(to_number_value), 111 | num_params: NumParams::Exactly(2), 112 | }, 113 | "%" => Operator { 114 | symbol: "%", 115 | operator: |items| js_op::abstract_mod(items[0], items[1]) 116 | .and_then(to_number_value), 117 | num_params: NumParams::Exactly(2), 118 | }, 119 | "max" => Operator { 120 | symbol: "max", 121 | operator: |items| js_op::abstract_max(items) 122 | .and_then(to_number_value), 123 | num_params: NumParams::AtLeast(1), 124 | }, 125 | "min" => Operator { 126 | symbol: "min", 127 | operator: |items| js_op::abstract_min(items) 128 | .and_then(to_number_value), 129 | num_params: NumParams::AtLeast(1), 130 | }, 131 | "merge" => Operator { 132 | symbol: "merge", 133 | operator: array::merge, 134 | num_params: NumParams::Any, 135 | }, 136 | "in" => Operator { 137 | symbol: "in", 138 | operator: array::in_, 139 | num_params: NumParams::Exactly(2), 140 | }, 141 | "cat" => Operator { 142 | symbol: "cat", 143 | operator: string::cat, 144 | num_params: NumParams::Any, 145 | }, 146 | "substr" => Operator { 147 | symbol: "substr", 148 | operator: string::substr, 149 | num_params: NumParams::Variadic(2..4), 150 | }, 151 | "log" => Operator { 152 | symbol: "log", 153 | operator: impure::log, 154 | num_params: NumParams::Unary, 155 | }, 156 | }; 157 | 158 | pub const DATA_OPERATOR_MAP: phf::Map<&'static str, DataOperator> = phf_map! { 159 | "var" => DataOperator { 160 | symbol: "var", 161 | operator: data::var, 162 | num_params: NumParams::Variadic(0..3) 163 | }, 164 | "missing" => DataOperator { 165 | symbol: "missing", 166 | operator: data::missing, 167 | num_params: NumParams::Any, 168 | }, 169 | "missing_some" => DataOperator { 170 | symbol: "missing_some", 171 | operator: data::missing_some, 172 | num_params: NumParams::Exactly(2), 173 | }, 174 | }; 175 | 176 | pub const LAZY_OPERATOR_MAP: phf::Map<&'static str, LazyOperator> = phf_map! { 177 | // Logical operators 178 | "if" => LazyOperator { 179 | symbol: "if", 180 | operator: logic::if_, 181 | num_params: NumParams::Any, 182 | }, 183 | // Note this operator isn't defined in the specification, but is 184 | // present in the tests as what looks like an alias for "if". 185 | "?:" => LazyOperator { 186 | symbol: "?:", 187 | operator: logic::if_, 188 | num_params: NumParams::Any, 189 | }, 190 | "or" => LazyOperator { 191 | symbol: "or", 192 | operator: logic::or, 193 | num_params: NumParams::AtLeast(1), 194 | }, 195 | "and" => LazyOperator { 196 | symbol: "and", 197 | operator: logic::and, 198 | num_params: NumParams::AtLeast(1), 199 | }, 200 | "map" => LazyOperator { 201 | symbol: "map", 202 | operator: array::map, 203 | num_params: NumParams::Exactly(2), 204 | }, 205 | "filter" => LazyOperator { 206 | symbol: "filter", 207 | operator: array::filter, 208 | num_params: NumParams::Exactly(2), 209 | }, 210 | "reduce" => LazyOperator { 211 | symbol: "reduce", 212 | operator: array::reduce, 213 | num_params: NumParams::Exactly(3), 214 | }, 215 | "all" => LazyOperator { 216 | symbol: "all", 217 | operator: array::all, 218 | num_params: NumParams::Exactly(2), 219 | }, 220 | "some" => LazyOperator { 221 | symbol: "some", 222 | operator: array::some, 223 | num_params: NumParams::Exactly(2), 224 | }, 225 | "none" => LazyOperator { 226 | symbol: "none", 227 | operator: array::none, 228 | num_params: NumParams::Exactly(2), 229 | }, 230 | }; 231 | 232 | #[derive(Debug, Clone)] 233 | pub enum NumParams { 234 | None, 235 | Any, 236 | Unary, 237 | Exactly(usize), 238 | AtLeast(usize), 239 | Variadic(std::ops::Range), // [inclusive, exclusive) 240 | } 241 | impl NumParams { 242 | fn is_valid_len(&self, len: &usize) -> bool { 243 | match self { 244 | Self::None => len == &0, 245 | Self::Any => true, 246 | Self::Unary => len == &1, 247 | Self::AtLeast(num) => len >= num, 248 | Self::Exactly(num) => len == num, 249 | Self::Variadic(range) => range.contains(len), 250 | } 251 | } 252 | fn check_len<'a>(&self, len: &'a usize) -> Result<&'a usize, Error> { 253 | match self.is_valid_len(len) { 254 | true => Ok(len), 255 | false => Err(Error::WrongArgumentCount { 256 | expected: self.clone(), 257 | actual: len.clone(), 258 | }), 259 | } 260 | } 261 | fn can_accept_unary(&self) -> bool { 262 | match self { 263 | Self::None => false, 264 | Self::Any => true, 265 | Self::Unary => true, 266 | Self::AtLeast(num) => num >= &1, 267 | Self::Exactly(num) => num == &1, 268 | Self::Variadic(range) => range.contains(&1), 269 | } 270 | } 271 | } 272 | 273 | trait CommonOperator { 274 | fn param_info(&self) -> &NumParams; 275 | } 276 | 277 | pub struct Operator { 278 | symbol: &'static str, 279 | operator: OperatorFn, 280 | num_params: NumParams, 281 | } 282 | impl Operator { 283 | pub fn execute(&self, items: &Vec<&Value>) -> Result { 284 | (self.operator)(items) 285 | } 286 | } 287 | impl CommonOperator for Operator { 288 | fn param_info(&self) -> &NumParams { 289 | &self.num_params 290 | } 291 | } 292 | impl fmt::Debug for Operator { 293 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 294 | f.debug_struct("Operator") 295 | .field("symbol", &self.symbol) 296 | .field("operator", &"") 297 | .finish() 298 | } 299 | } 300 | 301 | pub struct LazyOperator { 302 | symbol: &'static str, 303 | operator: LazyOperatorFn, 304 | num_params: NumParams, 305 | } 306 | impl LazyOperator { 307 | pub fn execute(&self, data: &Value, items: &Vec<&Value>) -> Result { 308 | (self.operator)(data, items) 309 | } 310 | } 311 | impl CommonOperator for LazyOperator { 312 | fn param_info(&self) -> &NumParams { 313 | &self.num_params 314 | } 315 | } 316 | impl fmt::Debug for LazyOperator { 317 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 318 | f.debug_struct("Operator") 319 | .field("symbol", &self.symbol) 320 | .field("operator", &"") 321 | .finish() 322 | } 323 | } 324 | 325 | /// An operator that operates on passed in data. 326 | /// 327 | /// Data operators' arguments can be lazily evaluated, but unlike 328 | /// regular operators, they still need access to data even after the 329 | /// evaluation of their arguments. 330 | pub struct DataOperator { 331 | symbol: &'static str, 332 | operator: DataOperatorFn, 333 | num_params: NumParams, 334 | } 335 | impl DataOperator { 336 | pub fn execute(&self, data: &Value, items: &Vec<&Value>) -> Result { 337 | (self.operator)(data, items) 338 | } 339 | } 340 | impl CommonOperator for DataOperator { 341 | fn param_info(&self) -> &NumParams { 342 | &self.num_params 343 | } 344 | } 345 | impl fmt::Debug for DataOperator { 346 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 347 | f.debug_struct("Operator") 348 | .field("symbol", &self.symbol) 349 | .field("operator", &"") 350 | .finish() 351 | } 352 | } 353 | 354 | type OperatorFn = fn(&Vec<&Value>) -> Result; 355 | type LazyOperatorFn = fn(&Value, &Vec<&Value>) -> Result; 356 | type DataOperatorFn = fn(&Value, &Vec<&Value>) -> Result; 357 | 358 | /// An operation that doesn't do any recursive parsing or evaluation. 359 | /// 360 | /// Any operator functions used must handle parsing of values themselves. 361 | #[derive(Debug)] 362 | pub struct LazyOperation<'a> { 363 | operator: &'a LazyOperator, 364 | arguments: Vec, 365 | } 366 | impl<'a> Parser<'a> for LazyOperation<'a> { 367 | fn from_value(value: &'a Value) -> Result, Error> { 368 | op_from_map(&LAZY_OPERATOR_MAP, value).and_then(|opt| { 369 | opt.map(|op| { 370 | Ok(LazyOperation { 371 | operator: op.op, 372 | arguments: op.args.into_iter().map(|v| v.clone()).collect(), 373 | }) 374 | }) 375 | .transpose() 376 | }) 377 | } 378 | 379 | fn evaluate(&self, data: &'a Value) -> Result { 380 | self.operator 381 | .execute(data, &self.arguments.iter().collect()) 382 | .map(Evaluated::New) 383 | } 384 | } 385 | 386 | impl From> for Value { 387 | fn from(op: LazyOperation) -> Value { 388 | let mut rv = Map::with_capacity(1); 389 | rv.insert( 390 | op.operator.symbol.into(), 391 | Value::Array(op.arguments.clone()), 392 | ); 393 | Value::Object(rv) 394 | } 395 | } 396 | 397 | #[derive(Debug)] 398 | pub struct Operation<'a> { 399 | operator: &'a Operator, 400 | arguments: Vec>, 401 | } 402 | impl<'a> Parser<'a> for Operation<'a> { 403 | fn from_value(value: &'a Value) -> Result, Error> { 404 | op_from_map(&OPERATOR_MAP, value).and_then(|opt| { 405 | opt.map(|op| { 406 | Ok(Operation { 407 | operator: op.op, 408 | arguments: Parsed::from_values(op.args)?, 409 | }) 410 | }) 411 | .transpose() 412 | }) 413 | } 414 | 415 | /// Evaluate the operation after recursively evaluating any nested operations 416 | fn evaluate(&self, data: &'a Value) -> Result { 417 | let arguments = self 418 | .arguments 419 | .iter() 420 | .map(|value| value.evaluate(data).map(Value::from)) 421 | .collect::, Error>>()?; 422 | self.operator 423 | .execute(&arguments.iter().collect()) 424 | .map(Evaluated::New) 425 | } 426 | } 427 | 428 | impl From> for Value { 429 | fn from(op: Operation) -> Value { 430 | let mut rv = Map::with_capacity(1); 431 | let values = op 432 | .arguments 433 | .into_iter() 434 | .map(Value::from) 435 | .collect::>(); 436 | rv.insert(op.operator.symbol.into(), Value::Array(values)); 437 | Value::Object(rv) 438 | } 439 | } 440 | 441 | #[derive(Debug)] 442 | pub struct DataOperation<'a> { 443 | operator: &'a DataOperator, 444 | arguments: Vec>, 445 | } 446 | impl<'a> Parser<'a> for DataOperation<'a> { 447 | fn from_value(value: &'a Value) -> Result, Error> { 448 | op_from_map(&DATA_OPERATOR_MAP, value).and_then(|opt| { 449 | opt.map(|op| { 450 | Ok(DataOperation { 451 | operator: op.op, 452 | arguments: Parsed::from_values(op.args)?, 453 | }) 454 | }) 455 | .transpose() 456 | }) 457 | } 458 | 459 | /// Evaluate the operation after recursively evaluating any nested operations 460 | fn evaluate(&self, data: &'a Value) -> Result { 461 | let arguments = self 462 | .arguments 463 | .iter() 464 | .map(|value| value.evaluate(data).map(Value::from)) 465 | .collect::, Error>>()?; 466 | self.operator 467 | .execute(data, &arguments.iter().collect()) 468 | .map(Evaluated::New) 469 | } 470 | } 471 | impl From> for Value { 472 | fn from(op: DataOperation) -> Value { 473 | let mut rv = Map::with_capacity(1); 474 | let values = op 475 | .arguments 476 | .into_iter() 477 | .map(Value::from) 478 | .collect::>(); 479 | rv.insert(op.operator.symbol.into(), Value::Array(values)); 480 | Value::Object(rv) 481 | } 482 | } 483 | 484 | struct OpArgs<'a, 'b, T> { 485 | op: &'a T, 486 | args: Vec<&'b Value>, 487 | } 488 | 489 | fn op_from_map<'a, 'b, T: CommonOperator>( 490 | map: &'a phf::Map<&'static str, T>, 491 | value: &'b Value, 492 | ) -> Result>, Error> { 493 | let obj = match value { 494 | Value::Object(obj) => obj, 495 | _ => return Ok(None), 496 | }; 497 | // With just one key. 498 | if obj.len() != 1 { 499 | return Ok(None); 500 | }; 501 | 502 | // We've already validated the length to be one, so any error 503 | // here is super unexpected. 504 | let key = obj.keys().next().ok_or_else(|| { 505 | Error::UnexpectedError(format!( 506 | "could not get first key from len(1) object: {:?}", 507 | obj 508 | )) 509 | })?; 510 | let val = obj.get(key).ok_or_else(|| { 511 | Error::UnexpectedError(format!( 512 | "could not get value for key '{}' from len(1) object: {:?}", 513 | key, obj 514 | )) 515 | })?; 516 | 517 | // See if the key is an operator. If it's not, return None. 518 | let op = match map.get(key.as_str()) { 519 | Some(op) => op, 520 | _ => return Ok(None), 521 | }; 522 | 523 | let err_for_non_unary = || { 524 | Err(Error::InvalidOperation { 525 | key: key.clone(), 526 | reason: "Arguments to non-unary operations must be arrays".into(), 527 | }) 528 | }; 529 | 530 | let param_info = op.param_info(); 531 | // If args value is not an array, and the operator is unary, 532 | // the value is treated as a unary argument array. 533 | let args = match val { 534 | Value::Array(args) => args.iter().collect::>(), 535 | _ => match param_info.can_accept_unary() { 536 | true => vec![val], 537 | false => return err_for_non_unary(), 538 | }, 539 | }; 540 | 541 | param_info.check_len(&args.len())?; 542 | 543 | Ok(Some(OpArgs { op, args })) 544 | } 545 | 546 | #[cfg(test)] 547 | mod test_operators { 548 | use super::*; 549 | 550 | /// All operators symbols must match their keys 551 | #[test] 552 | fn test_operator_map_symbols() { 553 | OPERATOR_MAP 554 | .into_iter() 555 | .for_each(|(k, op)| assert_eq!(*k, op.symbol)) 556 | } 557 | 558 | /// All lazy operators symbols must match their keys 559 | #[test] 560 | fn test_lazy_operator_map_symbols() { 561 | LAZY_OPERATOR_MAP 562 | .into_iter() 563 | .for_each(|(k, op)| assert_eq!(*k, op.symbol)) 564 | } 565 | } 566 | -------------------------------------------------------------------------------- /src/op/numeric.rs: -------------------------------------------------------------------------------- 1 | //! Numeric Operations 2 | 3 | use serde_json::Value; 4 | 5 | use crate::error::Error; 6 | use crate::js_op; 7 | use crate::value::to_number_value; 8 | 9 | fn compare(func: F, items: &Vec<&Value>) -> Result 10 | where 11 | F: Fn(&Value, &Value) -> bool, 12 | { 13 | if items.len() == 2 { 14 | Ok(Value::Bool(func(items[0], items[1]))) 15 | } else { 16 | Ok(Value::Bool( 17 | func(items[0], items[1]) && func(items[1], items[2]), 18 | )) 19 | } 20 | } 21 | 22 | /// Do < for either 2 or 3 values 23 | pub fn lt(items: &Vec<&Value>) -> Result { 24 | compare(js_op::abstract_lt, items) 25 | } 26 | 27 | /// Do <= for either 2 or 3 values 28 | pub fn lte(items: &Vec<&Value>) -> Result { 29 | compare(js_op::abstract_lte, items) 30 | } 31 | 32 | /// Do > for either 2 or 3 values 33 | pub fn gt(items: &Vec<&Value>) -> Result { 34 | compare(js_op::abstract_gt, items) 35 | } 36 | 37 | /// Do >= for either 2 or 3 values 38 | pub fn gte(items: &Vec<&Value>) -> Result { 39 | compare(js_op::abstract_gte, items) 40 | } 41 | 42 | /// Perform subtraction or convert a number to a negative 43 | pub fn minus(items: &Vec<&Value>) -> Result { 44 | let value = if items.len() == 1 { 45 | js_op::to_negative(items[0])? 46 | } else { 47 | js_op::abstract_minus(items[0], items[1])? 48 | }; 49 | to_number_value(value) 50 | } 51 | -------------------------------------------------------------------------------- /src/op/string.rs: -------------------------------------------------------------------------------- 1 | //! String Operations 2 | 3 | use serde_json::Value; 4 | use std::cmp; 5 | use std::convert::TryInto; 6 | 7 | use crate::error::Error; 8 | use crate::js_op; 9 | use crate::NULL; 10 | 11 | /// Concatenate strings. 12 | /// 13 | /// Note: the reference implementation just uses JS' builtin string 14 | /// concatenation with implicit casting, so e.g. `cast("foo", {})` 15 | /// evaluates to `"foo[object Object]". Here we explicitly require all 16 | /// arguments to be strings, because the specification explicitly defines 17 | /// `cat` as a string operation. 18 | pub fn cat(items: &Vec<&Value>) -> Result { 19 | let mut rv = String::from(""); 20 | items 21 | .into_iter() 22 | .map(|i| match i { 23 | Value::String(i_string) => Ok(i_string.clone()), 24 | _ => Ok(js_op::to_string(i)), 25 | }) 26 | .fold(Ok(&mut rv), |acc: Result<&mut String, Error>, i| { 27 | let rv = acc?; 28 | rv.push_str(&i?); 29 | Ok(rv) 30 | })?; 31 | Ok(Value::String(rv)) 32 | } 33 | 34 | /// Get a substring by index 35 | /// 36 | /// Note: the reference implementation casts the first argument to a string, 37 | /// but since the specification explicitly defines this as a string operation, 38 | /// the argument types are enforced here to avoid unpredictable behavior. 39 | pub fn substr(items: &Vec<&Value>) -> Result { 40 | // We can only have 2 or 3 arguments. Number of arguments is validated elsewhere. 41 | let (string_arg, idx_arg) = (items[0], items[1]); 42 | let limit_opt: Option<&Value>; 43 | if items.len() > 2 { 44 | limit_opt = Some(items[2]); 45 | } else { 46 | limit_opt = None; 47 | } 48 | 49 | let string = match string_arg { 50 | Value::String(s) => s, 51 | _ => { 52 | return Err(Error::InvalidArgument { 53 | value: string_arg.clone(), 54 | operation: "substr".into(), 55 | reason: "First argument to substr must be a string".into(), 56 | }) 57 | } 58 | }; 59 | let idx = match idx_arg { 60 | Value::Number(n) => { 61 | if let Some(int) = n.as_i64() { 62 | int 63 | } else { 64 | return Err(Error::InvalidArgument { 65 | value: idx_arg.clone(), 66 | operation: "substr".into(), 67 | reason: "Second argument to substr must be an integer".into(), 68 | }); 69 | } 70 | } 71 | _ => { 72 | return Err(Error::InvalidArgument { 73 | value: idx_arg.clone(), 74 | operation: "substr".into(), 75 | reason: "Second argument to substr must be a number".into(), 76 | }) 77 | } 78 | }; 79 | let limit = limit_opt 80 | .map(|limit_arg| match limit_arg { 81 | Value::Number(n) => { 82 | if let Some(int) = n.as_i64() { 83 | Ok(int) 84 | } else { 85 | Err(Error::InvalidArgument { 86 | value: limit_arg.clone(), 87 | operation: "substr".into(), 88 | reason: "Optional third argument to substr must be an integer".into(), 89 | }) 90 | } 91 | } 92 | _ => Err(Error::InvalidArgument { 93 | value: limit_arg.clone(), 94 | operation: "substr".into(), 95 | reason: "Optional third argument to substr must be a number".into(), 96 | }), 97 | }) 98 | .transpose()?; 99 | 100 | let string_len = string.len(); 101 | 102 | let idx_abs: usize = idx.abs().try_into().map_err(|e| Error::InvalidArgument { 103 | value: idx_arg.clone(), 104 | operation: "substr".into(), 105 | reason: format!( 106 | "The number {} is too large to index strings on this system", 107 | e 108 | ), 109 | })?; 110 | let start_idx = match idx { 111 | // If the index is negative it means "number of characters prior to the 112 | // end of the string from which to start", and corresponds to the string 113 | // length minus the index. 114 | idx if idx < 0 => string_len.checked_sub(idx_abs).unwrap_or(0), 115 | // A positive index is simply the starting point. Max starting point 116 | // is the length, which will yield an empty string. 117 | _ => cmp::min(string_len, idx_abs), 118 | }; 119 | 120 | let end_idx = match limit { 121 | None => string_len, 122 | Some(l) => { 123 | let limit_abs: usize = l.abs().try_into().map_err(|e| Error::InvalidArgument { 124 | value: limit_opt.or(Some(&NULL)).map(|v| v.clone()).unwrap(), 125 | operation: "substr".into(), 126 | reason: format!( 127 | "The number {} is too large to index strings on this system", 128 | e 129 | ), 130 | })?; 131 | match l { 132 | // If the limit is negative, it means "characters before the end 133 | // at which to stop", corresponding to an index of either 0 or 134 | // the length of the string minus the limit. 135 | l if l < 0 => string_len.checked_sub(limit_abs).unwrap_or(0), 136 | // A positive limit indicates the number of characters to take, 137 | // so it corresponds to an index of the start index plus the 138 | // limit (with a maximum value of the string length). 139 | _ => cmp::min( 140 | string_len, 141 | start_idx.checked_add(limit_abs).unwrap_or(string_len), 142 | ), 143 | } 144 | } 145 | }; 146 | 147 | let count_in_substr = end_idx.checked_sub(start_idx).unwrap_or(0); 148 | 149 | // Iter over our expected count rather than indexing directly to avoid 150 | // potential panics if any of our math is wrong. 151 | Ok(Value::String( 152 | string 153 | .chars() 154 | .skip(start_idx) 155 | .take(count_in_substr) 156 | .collect(), 157 | )) 158 | } 159 | -------------------------------------------------------------------------------- /src/value.rs: -------------------------------------------------------------------------------- 1 | use serde_json::{Number, Value}; 2 | 3 | use crate::error::Error; 4 | use crate::op::{DataOperation, LazyOperation, Operation}; 5 | use crate::Parser; 6 | 7 | /// A Parsed JSON value 8 | /// 9 | /// Parsed values are one of: 10 | /// - An operation whose arguments are eagerly evaluated 11 | /// - An operation whose arguments are lazily evaluated 12 | /// - A raw value: a non-rule, raw JSON value 13 | #[derive(Debug)] 14 | pub enum Parsed<'a> { 15 | Operation(Operation<'a>), 16 | LazyOperation(LazyOperation<'a>), 17 | DataOperation(DataOperation<'a>), 18 | Raw(Raw<'a>), 19 | } 20 | impl<'a> Parsed<'a> { 21 | /// Recursively parse a value 22 | pub fn from_value(value: &'a Value) -> Result { 23 | Operation::from_value(value)? 24 | .map(Self::Operation) 25 | // .or(Operation::from_value(value)?.map(Self::Operation)) 26 | .or(LazyOperation::from_value(value)?.map(Self::LazyOperation)) 27 | .or(DataOperation::from_value(value)?.map(Self::DataOperation)) 28 | .or(Raw::from_value(value)?.map(Self::Raw)) 29 | .ok_or_else(|| { 30 | Error::UnexpectedError(format!("Failed to parse Value {:?}", value)) 31 | }) 32 | } 33 | 34 | pub fn from_values(values: Vec<&'a Value>) -> Result, Error> { 35 | values 36 | .into_iter() 37 | .map(Self::from_value) 38 | .collect::, Error>>() 39 | } 40 | 41 | pub fn evaluate(&self, data: &'a Value) -> Result { 42 | match self { 43 | Self::Operation(op) => op.evaluate(data), 44 | Self::LazyOperation(op) => op.evaluate(data), 45 | Self::DataOperation(op) => op.evaluate(data), 46 | Self::Raw(val) => val.evaluate(data), 47 | } 48 | } 49 | } 50 | impl From> for Value { 51 | fn from(item: Parsed) -> Value { 52 | match item { 53 | Parsed::Operation(op) => Value::from(op), 54 | Parsed::LazyOperation(op) => Value::from(op), 55 | Parsed::DataOperation(op) => Value::from(op), 56 | Parsed::Raw(raw) => Value::from(raw), 57 | } 58 | } 59 | } 60 | 61 | /// A Raw JSON value 62 | /// 63 | /// Raw values are those that are not any known operation. A raw value may 64 | /// be of any valid JSON type. 65 | #[derive(Debug)] 66 | pub struct Raw<'a> { 67 | value: &'a Value, 68 | } 69 | impl<'a> Parser<'a> for Raw<'a> { 70 | fn from_value(value: &'a Value) -> Result, Error> { 71 | Ok(Some(Self { value })) 72 | } 73 | fn evaluate(&self, _data: &Value) -> Result { 74 | Ok(Evaluated::Raw(self.value)) 75 | } 76 | } 77 | impl From> for Value { 78 | fn from(raw: Raw) -> Self { 79 | raw.value.clone() 80 | } 81 | } 82 | 83 | /// An Evaluated JSON value 84 | /// 85 | /// An evaluated value is one of: 86 | /// - A new value: either a calculated Rule or a filled Variable 87 | /// - A raw value: a non-rule, raw JSON value 88 | #[derive(Debug)] 89 | pub enum Evaluated<'a> { 90 | New(Value), 91 | Raw(&'a Value), 92 | } 93 | 94 | impl From> for Value { 95 | fn from(item: Evaluated) -> Self { 96 | match item { 97 | Evaluated::Raw(val) => val.clone(), 98 | Evaluated::New(val) => val, 99 | } 100 | } 101 | } 102 | 103 | pub fn to_number_value(number: f64) -> Result { 104 | if number.fract() == 0.0 { 105 | Ok(Value::Number(Number::from(number as i64))) 106 | } else { 107 | Number::from_f64(number) 108 | .ok_or_else(|| { 109 | Error::UnexpectedError(format!( 110 | "Could not make JSON number from result {:?}", 111 | number 112 | )) 113 | }) 114 | .map(Value::Number) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # Integration Tests 2 | 3 | The `test_lib.rs` use the test JSON available from the JSONLogic 4 | project to validate teh Rust library. The test JSON is checked in under 5 | `data/tests.json`. ~~When tests run, the content of `tests.json` is 6 | validated against the most recent content of the tests returned from 7 | the server. If they don't match, the test fails.~~ 8 | 9 | We run that full suite of tests against all implementations. 10 | -------------------------------------------------------------------------------- /tests/data/tests.json: -------------------------------------------------------------------------------- 1 | [ 2 | "# Non-rules get passed through", 3 | [ true, {}, true ], 4 | [ false, {}, false ], 5 | [ 17, {}, 17 ], 6 | [ 3.14, {}, 3.14 ], 7 | [ "apple", {}, "apple" ], 8 | [ null, {}, null ], 9 | [ ["a","b"], {}, ["a","b"] ], 10 | 11 | "# Single operator tests", 12 | [ {"==":[1,1]}, {}, true ], 13 | [ {"==":[1,"1"]}, {}, true ], 14 | [ {"==":[1,2]}, {}, false ], 15 | [ {"===":[1,1]}, {}, true ], 16 | [ {"===":[1,"1"]}, {}, false ], 17 | [ {"===":[1,2]}, {}, false ], 18 | [ {"!=":[1,2]}, {}, true ], 19 | [ {"!=":[1,1]}, {}, false ], 20 | [ {"!=":[1,"1"]}, {}, false ], 21 | [ {"!==":[1,2]}, {}, true ], 22 | [ {"!==":[1,1]}, {}, false ], 23 | [ {"!==":[1,"1"]}, {}, true ], 24 | [ {">":[2,1]}, {}, true ], 25 | [ {">":[1,1]}, {}, false ], 26 | [ {">":[1,2]}, {}, false ], 27 | [ {">":["2",1]}, {}, true ], 28 | [ {">=":[2,1]}, {}, true ], 29 | [ {">=":[1,1]}, {}, true ], 30 | [ {">=":[1,2]}, {}, false ], 31 | [ {">=":["2",1]}, {}, true ], 32 | [ {"<":[2,1]}, {}, false ], 33 | [ {"<":[1,1]}, {}, false ], 34 | [ {"<":[1,2]}, {}, true ], 35 | [ {"<":["1",2]}, {}, true ], 36 | [ {"<":[1,2,3]}, {}, true ], 37 | [ {"<":[1,1,3]}, {}, false ], 38 | [ {"<":[1,4,3]}, {}, false ], 39 | [ {"<=":[2,1]}, {}, false ], 40 | [ {"<=":[1,1]}, {}, true ], 41 | [ {"<=":[1,2]}, {}, true ], 42 | [ {"<=":["1",2]}, {}, true ], 43 | [ {"<=":[1,2,3]}, {}, true ], 44 | [ {"<=":[1,4,3]}, {}, false ], 45 | [ {"!":[false]}, {}, true ], 46 | [ {"!":false}, {}, true ], 47 | [ {"!":[true]}, {}, false ], 48 | [ {"!":true}, {}, false ], 49 | [ {"!":0}, {}, true ], 50 | [ {"!":1}, {}, false ], 51 | [ {"or":[true,true]}, {}, true ], 52 | [ {"or":[false,true]}, {}, true ], 53 | [ {"or":[true,false]}, {}, true ], 54 | [ {"or":[false,false]}, {}, false ], 55 | [ {"or":[false,false,true]}, {}, true ], 56 | [ {"or":[false,false,false]}, {}, false ], 57 | [ {"or":[false]}, {}, false ], 58 | [ {"or":[true]}, {}, true ], 59 | [ {"or":[1,3]}, {}, 1 ], 60 | [ {"or":[3,false]}, {}, 3 ], 61 | [ {"or":[false,3]}, {}, 3 ], 62 | [ {"and":[true,true]}, {}, true ], 63 | [ {"and":[false,true]}, {}, false ], 64 | [ {"and":[true,false]}, {}, false ], 65 | [ {"and":[false,false]}, {}, false ], 66 | [ {"and":[true,true,true]}, {}, true ], 67 | [ {"and":[true,true,false]}, {}, false ], 68 | [ {"and":[false]}, {}, false ], 69 | [ {"and":[true]}, {}, true ], 70 | [ {"and":[1,3]}, {}, 3 ], 71 | [ {"and":[3,false]}, {}, false ], 72 | [ {"and":[false,3]}, {}, false ], 73 | [ {"?:":[true,1,2]}, {}, 1 ], 74 | [ {"?:":[false,1,2]}, {}, 2 ], 75 | [ {"in":["Bart",["Bart","Homer","Lisa","Marge","Maggie"]]}, {}, true ], 76 | [ {"in":["Milhouse",["Bart","Homer","Lisa","Marge","Maggie"]]}, {}, false ], 77 | [ {"in":["Spring","Springfield"]}, {}, true ], 78 | [ {"in":["i","team"]}, {}, false ], 79 | [ {"cat":"ice"}, {}, "ice" ], 80 | [ {"cat":["ice"]}, {}, "ice" ], 81 | [ {"cat":["ice","cream"]}, {}, "icecream" ], 82 | [ {"cat":[1,2]}, {}, "12" ], 83 | [ {"cat":["Robocop",2]}, {}, "Robocop2" ], 84 | [ {"cat":["we all scream for ","ice","cream"]}, {}, "we all scream for icecream" ], 85 | [ {"%":[1,2]}, {}, 1 ], 86 | [ {"%":[2,2]}, {}, 0 ], 87 | [ {"%":[3,2]}, {}, 1 ], 88 | [ {"max":[1,2,3]}, {}, 3 ], 89 | [ {"max":[1,3,3]}, {}, 3 ], 90 | [ {"max":[3,2,1]}, {}, 3 ], 91 | [ {"max":[1]}, {}, 1 ], 92 | [ {"min":[1,2,3]}, {}, 1 ], 93 | [ {"min":[1,1,3]}, {}, 1 ], 94 | [ {"min":[3,2,1]}, {}, 1 ], 95 | [ {"min":[1]}, {}, 1 ], 96 | 97 | [ {"+":[1,2]}, {}, 3 ], 98 | [ {"+":[2,2,2]}, {}, 6 ], 99 | [ {"+":[1]}, {}, 1 ], 100 | [ {"+":["1",1]}, {}, 2 ], 101 | [ {"*":[3,2]}, {}, 6 ], 102 | [ {"*":[2,2,2]}, {}, 8 ], 103 | [ {"*":[1]}, {}, 1 ], 104 | [ {"*":["1",1]}, {}, 1 ], 105 | [ {"-":[2,3]}, {}, -1 ], 106 | [ {"-":[3,2]}, {}, 1 ], 107 | [ {"-":[3]}, {}, -3 ], 108 | [ {"-":["1",1]}, {}, 0 ], 109 | [ {"/":[4,2]}, {}, 2 ], 110 | [ {"/":[2,4]}, {}, 0.5 ], 111 | [ {"/":["1",1]}, {}, 1 ], 112 | 113 | "Substring", 114 | [{"substr":["jsonlogic", 4]}, null, "logic"], 115 | [{"substr":["jsonlogic", -5]}, null, "logic"], 116 | [{"substr":["jsonlogic", 0, 1]}, null, "j"], 117 | [{"substr":["jsonlogic", -1, 1]}, null, "c"], 118 | [{"substr":["jsonlogic", 4, 5]}, null, "logic"], 119 | [{"substr":["jsonlogic", -5, 5]}, null, "logic"], 120 | [{"substr":["jsonlogic", -5, -2]}, null, "log"], 121 | [{"substr":["jsonlogic", 1, -5]}, null, "son"], 122 | 123 | "Merge arrays", 124 | [{"merge":[]}, null, []], 125 | [{"merge":[[1]]}, null, [1]], 126 | [{"merge":[[1],[]]}, null, [1]], 127 | [{"merge":[[1], [2]]}, null, [1,2]], 128 | [{"merge":[[1], [2], [3]]}, null, [1,2,3]], 129 | [{"merge":[[1, 2], [3]]}, null, [1,2,3]], 130 | [{"merge":[[1], [2, 3]]}, null, [1,2,3]], 131 | "Given non-array arguments, merge converts them to arrays", 132 | [{"merge":1}, null, [1]], 133 | [{"merge":[1,2]}, null, [1,2]], 134 | [{"merge":[1,[2]]}, null, [1,2]], 135 | 136 | "Too few args", 137 | [{"if":[]}, null, null], 138 | [{"if":[true]}, null, true], 139 | [{"if":[false]}, null, false], 140 | [{"if":["apple"]}, null, "apple"], 141 | 142 | "Simple if/then/else cases", 143 | [{"if":[true, "apple"]}, null, "apple"], 144 | [{"if":[false, "apple"]}, null, null], 145 | [{"if":[true, "apple", "banana"]}, null, "apple"], 146 | [{"if":[false, "apple", "banana"]}, null, "banana"], 147 | 148 | "Empty arrays are falsey", 149 | [{"if":[ [], "apple", "banana"]}, null, "banana"], 150 | [{"if":[ [1], "apple", "banana"]}, null, "apple"], 151 | [{"if":[ [1,2,3,4], "apple", "banana"]}, null, "apple"], 152 | 153 | "Empty strings are falsey, all other strings are truthy", 154 | [{"if":[ "", "apple", "banana"]}, null, "banana"], 155 | [{"if":[ "zucchini", "apple", "banana"]}, null, "apple"], 156 | [{"if":[ "0", "apple", "banana"]}, null, "apple"], 157 | 158 | "You can cast a string to numeric with a unary + ", 159 | [{"===":[0,"0"]}, null, false], 160 | [{"===":[0,{"+":"0"}]}, null, true], 161 | [{"if":[ {"+":"0"}, "apple", "banana"]}, null, "banana"], 162 | [{"if":[ {"+":"1"}, "apple", "banana"]}, null, "apple"], 163 | 164 | "Zero is falsy, all other numbers are truthy", 165 | [{"if":[ 0, "apple", "banana"]}, null, "banana"], 166 | [{"if":[ 1, "apple", "banana"]}, null, "apple"], 167 | [{"if":[ 3.1416, "apple", "banana"]}, null, "apple"], 168 | [{"if":[ -1, "apple", "banana"]}, null, "apple"], 169 | 170 | "Truthy and falsy definitions matter in Boolean operations", 171 | [{"!" : [ [] ]}, {}, true], 172 | [{"!!" : [ [] ]}, {}, false], 173 | [{"and" : [ [], true ]}, {}, [] ], 174 | [{"or" : [ [], true ]}, {}, true ], 175 | 176 | [{"!" : [ 0 ]}, {}, true], 177 | [{"!!" : [ 0 ]}, {}, false], 178 | [{"and" : [ 0, true ]}, {}, 0 ], 179 | [{"or" : [ 0, true ]}, {}, true ], 180 | 181 | [{"!" : [ "" ]}, {}, true], 182 | [{"!!" : [ "" ]}, {}, false], 183 | [{"and" : [ "", true ]}, {}, "" ], 184 | [{"or" : [ "", true ]}, {}, true ], 185 | 186 | [{"!" : [ "0" ]}, {}, false], 187 | [{"!!" : [ "0" ]}, {}, true], 188 | [{"and" : [ "0", true ]}, {}, true ], 189 | [{"or" : [ "0", true ]}, {}, "0" ], 190 | 191 | "If the conditional is logic, it gets evaluated", 192 | [{"if":[ {">":[2,1]}, "apple", "banana"]}, null, "apple"], 193 | [{"if":[ {">":[1,2]}, "apple", "banana"]}, null, "banana"], 194 | 195 | "If the consequents are logic, they get evaluated", 196 | [{"if":[ true, {"cat":["ap","ple"]}, {"cat":["ba","na","na"]} ]}, null, "apple"], 197 | [{"if":[ false, {"cat":["ap","ple"]}, {"cat":["ba","na","na"]} ]}, null, "banana"], 198 | 199 | "If/then/elseif/then cases", 200 | [{"if":[true, "apple", true, "banana"]}, null, "apple"], 201 | [{"if":[true, "apple", false, "banana"]}, null, "apple"], 202 | [{"if":[false, "apple", true, "banana"]}, null, "banana"], 203 | [{"if":[false, "apple", false, "banana"]}, null, null], 204 | 205 | [{"if":[true, "apple", true, "banana", "carrot"]}, null, "apple"], 206 | [{"if":[true, "apple", false, "banana", "carrot"]}, null, "apple"], 207 | [{"if":[false, "apple", true, "banana", "carrot"]}, null, "banana"], 208 | [{"if":[false, "apple", false, "banana", "carrot"]}, null, "carrot"], 209 | 210 | [{"if":[false, "apple", false, "banana", false, "carrot"]}, null, null], 211 | [{"if":[false, "apple", false, "banana", false, "carrot", "date"]}, null, "date"], 212 | [{"if":[false, "apple", false, "banana", true, "carrot", "date"]}, null, "carrot"], 213 | [{"if":[false, "apple", true, "banana", false, "carrot", "date"]}, null, "banana"], 214 | [{"if":[false, "apple", true, "banana", true, "carrot", "date"]}, null, "banana"], 215 | [{"if":[true, "apple", false, "banana", false, "carrot", "date"]}, null, "apple"], 216 | [{"if":[true, "apple", false, "banana", true, "carrot", "date"]}, null, "apple"], 217 | [{"if":[true, "apple", true, "banana", false, "carrot", "date"]}, null, "apple"], 218 | [{"if":[true, "apple", true, "banana", true, "carrot", "date"]}, null, "apple"], 219 | 220 | "# Compound Tests", 221 | [ {"and":[{">":[3,1]},true]}, {}, true ], 222 | [ {"and":[{">":[3,1]},false]}, {}, false ], 223 | [ {"and":[{">":[3,1]},{"!":true}]}, {}, false ], 224 | [ {"and":[{">":[3,1]},{"<":[1,3]}]}, {}, true ], 225 | [ {"?:":[{">":[3,1]},"visible","hidden"]}, {}, "visible" ], 226 | 227 | "# Data-Driven", 228 | [ {"var":["a"]},{"a":1},1 ], 229 | [ {"var":["b"]},{"a":1},null ], 230 | [ {"var":["a"]},null,null ], 231 | [ {"var":"a"},{"a":1},1 ], 232 | [ {"var":"b"},{"a":1},null ], 233 | [ {"var":"a"},null,null ], 234 | [ {"var":["a", 1]},null,1 ], 235 | [ {"var":["b", 2]},{"a":1},2 ], 236 | [ {"var":"a.b"},{"a":{"b":"c"}},"c" ], 237 | [ {"var":"a.q"},{"a":{"b":"c"}},null ], 238 | [ {"var":["a.q", 9]},{"a":{"b":"c"}},9 ], 239 | [ {"var":1}, ["apple","banana"], "banana" ], 240 | [ {"var":"1"}, ["apple","banana"], "banana" ], 241 | [ {"var":"1.1"}, ["apple",["banana","beer"]], "beer" ], 242 | [ {"and":[{"<":[{"var":"temp"},110]},{"==":[{"var":"pie.filling"},"apple"]}]},{"temp":100,"pie":{"filling":"apple"}},true ], 243 | [ {"var":[{"?:":[{"<":[{"var":"temp"},110]},"pie.filling","pie.eta"]}]},{"temp":100,"pie":{"filling":"apple","eta":"60s"}},"apple" ], 244 | [ {"in":[{"var":"filling"},["apple","cherry"]]},{"filling":"apple"},true ], 245 | [ {"var":"a.b.c"}, null, null ], 246 | [ {"var":"a.b.c"}, {"a":null}, null ], 247 | [ {"var":"a.b.c"}, {"a":{"b":null}}, null ], 248 | [ {"var":""}, 1, 1 ], 249 | [ {"var":null}, 1, 1 ], 250 | [ {"var":[]}, 1, 1 ], 251 | 252 | "Missing", 253 | [{"missing":[]}, null, []], 254 | [{"missing":["a"]}, null, ["a"]], 255 | [{"missing":"a"}, null, ["a"]], 256 | [{"missing":"a"}, {"a":"apple"}, []], 257 | [{"missing":["a"]}, {"a":"apple"}, []], 258 | [{"missing":["a","b"]}, {"a":"apple"}, ["b"]], 259 | [{"missing":["a","b"]}, {"b":"banana"}, ["a"]], 260 | [{"missing":["a","b"]}, {"a":"apple", "b":"banana"}, []], 261 | [{"missing":["a","b"]}, {}, ["a","b"]], 262 | [{"missing":["a","b"]}, null, ["a","b"]], 263 | 264 | [{"missing":["a.b"]}, null, ["a.b"]], 265 | [{"missing":["a.b"]}, {"a":"apple"}, ["a.b"]], 266 | [{"missing":["a.b"]}, {"a":{"c":"apple cake"}}, ["a.b"]], 267 | [{"missing":["a.b"]}, {"a":{"b":"apple brownie"}}, []], 268 | [{"missing":["a.b", "a.c"]}, {"a":{"b":"apple brownie"}}, ["a.c"]], 269 | 270 | 271 | "Missing some", 272 | [{"missing_some":[1, ["a", "b"]]}, {"a":"apple"}, [] ], 273 | [{"missing_some":[1, ["a", "b"]]}, {"b":"banana"}, [] ], 274 | [{"missing_some":[1, ["a", "b"]]}, {"a":"apple", "b":"banana"}, [] ], 275 | [{"missing_some":[1, ["a", "b"]]}, {"c":"carrot"}, ["a", "b"]], 276 | 277 | [{"missing_some":[2, ["a", "b", "c"]]}, {"a":"apple", "b":"banana"}, [] ], 278 | [{"missing_some":[2, ["a", "b", "c"]]}, {"a":"apple", "c":"carrot"}, [] ], 279 | [{"missing_some":[2, ["a", "b", "c"]]}, {"a":"apple", "b":"banana", "c":"carrot"}, [] ], 280 | [{"missing_some":[2, ["a", "b", "c"]]}, {"a":"apple", "d":"durian"}, ["b", "c"] ], 281 | [{"missing_some":[2, ["a", "b", "c"]]}, {"d":"durian", "e":"eggplant"}, ["a", "b", "c"] ], 282 | 283 | 284 | "Missing and If are friends, because empty arrays are falsey in JsonLogic", 285 | [{"if":[ {"missing":"a"}, "missed it", "found it" ]}, {"a":"apple"}, "found it"], 286 | [{"if":[ {"missing":"a"}, "missed it", "found it" ]}, {"b":"banana"}, "missed it"], 287 | 288 | "Missing, Merge, and If are friends. VIN is always required, APR is only required if financing is true.", 289 | [ 290 | {"missing":{"merge":[ "vin", {"if": [{"var":"financing"}, ["apr"], [] ]} ]} }, 291 | {"financing":true}, 292 | ["vin","apr"] 293 | ], 294 | 295 | [ 296 | {"missing":{"merge":[ "vin", {"if": [{"var":"financing"}, ["apr"], [] ]} ]} }, 297 | {"financing":false}, 298 | ["vin"] 299 | ], 300 | 301 | "Filter, map, all, none, and some", 302 | [ 303 | {"filter":[{"var":"integers"}, true]}, 304 | {"integers":[1,2,3]}, 305 | [1,2,3] 306 | ], 307 | [ 308 | {"filter":[{"var":"integers"}, false]}, 309 | {"integers":[1,2,3]}, 310 | [] 311 | ], 312 | [ 313 | {"filter":[{"var":"integers"}, {">=":[{"var":""},2]}]}, 314 | {"integers":[1,2,3]}, 315 | [2,3] 316 | ], 317 | [ 318 | {"filter":[{"var":"integers"}, {"%":[{"var":""},2]}]}, 319 | {"integers":[1,2,3]}, 320 | [1,3] 321 | ], 322 | 323 | [ 324 | {"map":[{"var":"integers"}, {"*":[{"var":""},2]}]}, 325 | {"integers":[1,2,3]}, 326 | [2,4,6] 327 | ], 328 | [ 329 | {"map":[{"var":"integers"}, {"*":[{"var":""},2]}]}, 330 | null, 331 | [] 332 | ], 333 | [ 334 | {"map":[{"var":"desserts"}, {"var":"qty"}]}, 335 | {"desserts":[ 336 | {"name":"apple","qty":1}, 337 | {"name":"brownie","qty":2}, 338 | {"name":"cupcake","qty":3} 339 | ]}, 340 | [1,2,3] 341 | ], 342 | 343 | [ 344 | {"reduce":[ 345 | {"var":"integers"}, 346 | {"+":[{"var":"current"}, {"var":"accumulator"}]}, 347 | 0 348 | ]}, 349 | {"integers":[1,2,3,4]}, 350 | 10 351 | ], 352 | [ 353 | {"reduce":[ 354 | {"var":"integers"}, 355 | {"+":[{"var":"current"}, {"var":"accumulator"}]}, 356 | 0 357 | ]}, 358 | null, 359 | 0 360 | ], 361 | [ 362 | {"reduce":[ 363 | {"var":"integers"}, 364 | {"*":[{"var":"current"}, {"var":"accumulator"}]}, 365 | 1 366 | ]}, 367 | {"integers":[1,2,3,4]}, 368 | 24 369 | ], 370 | [ 371 | {"reduce":[ 372 | {"var":"integers"}, 373 | {"*":[{"var":"current"}, {"var":"accumulator"}]}, 374 | 0 375 | ]}, 376 | {"integers":[1,2,3,4]}, 377 | 0 378 | ], 379 | [ 380 | {"reduce": [ 381 | {"var":"desserts"}, 382 | {"+":[ {"var":"accumulator"}, {"var":"current.qty"}]}, 383 | 0 384 | ]}, 385 | {"desserts":[ 386 | {"name":"apple","qty":1}, 387 | {"name":"brownie","qty":2}, 388 | {"name":"cupcake","qty":3} 389 | ]}, 390 | 6 391 | ], 392 | 393 | 394 | [ 395 | {"all":[{"var":"integers"}, {">=":[{"var":""}, 1]}]}, 396 | {"integers":[1,2,3]}, 397 | true 398 | ], 399 | [ 400 | {"all":[{"var":"integers"}, {"==":[{"var":""}, 1]}]}, 401 | {"integers":[1,2,3]}, 402 | false 403 | ], 404 | [ 405 | {"all":[{"var":"integers"}, {"<":[{"var":""}, 1]}]}, 406 | {"integers":[1,2,3]}, 407 | false 408 | ], 409 | [ 410 | {"all":[{"var":"integers"}, {"<":[{"var":""}, 1]}]}, 411 | {"integers":[]}, 412 | false 413 | ], 414 | [ 415 | {"all":[ {"var":"items"}, {">=":[{"var":"qty"}, 1]}]}, 416 | {"items":[{"qty":1,"sku":"apple"},{"qty":2,"sku":"banana"}]}, 417 | true 418 | ], 419 | [ 420 | {"all":[ {"var":"items"}, {">":[{"var":"qty"}, 1]}]}, 421 | {"items":[{"qty":1,"sku":"apple"},{"qty":2,"sku":"banana"}]}, 422 | false 423 | ], 424 | [ 425 | {"all":[ {"var":"items"}, {"<":[{"var":"qty"}, 1]}]}, 426 | {"items":[{"qty":1,"sku":"apple"},{"qty":2,"sku":"banana"}]}, 427 | false 428 | ], 429 | [ 430 | {"all":[ {"var":"items"}, {">=":[{"var":"qty"}, 1]}]}, 431 | {"items":[]}, 432 | false 433 | ], 434 | 435 | 436 | [ 437 | {"none":[{"var":"integers"}, {">=":[{"var":""}, 1]}]}, 438 | {"integers":[1,2,3]}, 439 | false 440 | ], 441 | [ 442 | {"none":[{"var":"integers"}, {"==":[{"var":""}, 1]}]}, 443 | {"integers":[1,2,3]}, 444 | false 445 | ], 446 | [ 447 | {"none":[{"var":"integers"}, {"<":[{"var":""}, 1]}]}, 448 | {"integers":[1,2,3]}, 449 | true 450 | ], 451 | [ 452 | {"none":[{"var":"integers"}, {"<":[{"var":""}, 1]}]}, 453 | {"integers":[]}, 454 | true 455 | ], 456 | [ 457 | {"none":[ {"var":"items"}, {">=":[{"var":"qty"}, 1]}]}, 458 | {"items":[{"qty":1,"sku":"apple"},{"qty":2,"sku":"banana"}]}, 459 | false 460 | ], 461 | [ 462 | {"none":[ {"var":"items"}, {">":[{"var":"qty"}, 1]}]}, 463 | {"items":[{"qty":1,"sku":"apple"},{"qty":2,"sku":"banana"}]}, 464 | false 465 | ], 466 | [ 467 | {"none":[ {"var":"items"}, {"<":[{"var":"qty"}, 1]}]}, 468 | {"items":[{"qty":1,"sku":"apple"},{"qty":2,"sku":"banana"}]}, 469 | true 470 | ], 471 | [ 472 | {"none":[ {"var":"items"}, {">=":[{"var":"qty"}, 1]}]}, 473 | {"items":[]}, 474 | true 475 | ], 476 | 477 | [ 478 | {"some":[{"var":"integers"}, {">=":[{"var":""}, 1]}]}, 479 | {"integers":[1,2,3]}, 480 | true 481 | ], 482 | [ 483 | {"some":[{"var":"integers"}, {"==":[{"var":""}, 1]}]}, 484 | {"integers":[1,2,3]}, 485 | true 486 | ], 487 | [ 488 | {"some":[{"var":"integers"}, {"<":[{"var":""}, 1]}]}, 489 | {"integers":[1,2,3]}, 490 | false 491 | ], 492 | [ 493 | {"some":[{"var":"integers"}, {"<":[{"var":""}, 1]}]}, 494 | {"integers":[]}, 495 | false 496 | ], 497 | [ 498 | {"some":[ {"var":"items"}, {">=":[{"var":"qty"}, 1]}]}, 499 | {"items":[{"qty":1,"sku":"apple"},{"qty":2,"sku":"banana"}]}, 500 | true 501 | ], 502 | [ 503 | {"some":[ {"var":"items"}, {">":[{"var":"qty"}, 1]}]}, 504 | {"items":[{"qty":1,"sku":"apple"},{"qty":2,"sku":"banana"}]}, 505 | true 506 | ], 507 | [ 508 | {"some":[ {"var":"items"}, {"<":[{"var":"qty"}, 1]}]}, 509 | {"items":[{"qty":1,"sku":"apple"},{"qty":2,"sku":"banana"}]}, 510 | false 511 | ], 512 | [ 513 | {"some":[ {"var":"items"}, {">=":[{"var":"qty"}, 1]}]}, 514 | {"items":[]}, 515 | false 516 | ], 517 | 518 | "EOF" 519 | ] 520 | -------------------------------------------------------------------------------- /tests/test_lib.rs: -------------------------------------------------------------------------------- 1 | //! Run the official tests from the web. 2 | 3 | use std::fs::File; 4 | use std::io::prelude::*; 5 | use std::path::Path; 6 | 7 | use reqwest; 8 | use serde_json; 9 | use serde_json::Value; 10 | 11 | use jsonlogic_rs; 12 | 13 | struct TestCase { 14 | logic: Value, 15 | data: Value, 16 | result: Value, 17 | } 18 | 19 | const TEST_URL: &'static str = "http://jsonlogic.com/tests.json"; 20 | 21 | fn load_file_json() -> Value { 22 | let mut file = File::open(Path::join( 23 | Path::new(file!()).parent().unwrap(), 24 | "data/tests.json", 25 | )) 26 | .unwrap(); 27 | let mut contents = String::new(); 28 | file.read_to_string(&mut contents).unwrap(); 29 | serde_json::from_str(&contents).unwrap() 30 | } 31 | 32 | fn load_tests() -> Vec { 33 | let loaded_json = load_file_json(); 34 | let cases = match loaded_json { 35 | Value::Array(cases) => cases, 36 | _ => panic!("cases aren't array"), 37 | }; 38 | cases 39 | .into_iter() 40 | .filter_map(|case| match case { 41 | Value::Array(data) => Some(TestCase { 42 | logic: data[0].clone(), 43 | data: data[1].clone(), 44 | result: data[2].clone(), 45 | }), 46 | Value::String(_) => None, 47 | _ => panic!("case can't be destructured!"), 48 | }) 49 | .collect() 50 | } 51 | 52 | #[test] 53 | #[ignore] 54 | fn check_test_file() { 55 | let resp_res = reqwest::blocking::get(TEST_URL).unwrap().text(); 56 | let resp = match resp_res { 57 | Ok(r) => r, 58 | Err(e) => { 59 | println!("Failed to get new version of test JSON: {:?}", e); 60 | return (); 61 | } 62 | }; 63 | let http_json: Value = serde_json::from_str(&resp).unwrap(); 64 | let file_json = load_file_json(); 65 | assert_eq!(http_json, file_json); 66 | } 67 | 68 | #[test] 69 | fn run_cases() { 70 | let cases = load_tests(); 71 | cases.into_iter().for_each(|case| { 72 | println!("Running case"); 73 | println!(" logic: {:?}", case.logic); 74 | println!(" data: {:?}", case.data); 75 | println!(" expected: {:?}", case.result); 76 | assert_eq!( 77 | jsonlogic_rs::apply(&case.logic, &case.data).unwrap(), 78 | case.result 79 | ) 80 | }) 81 | } 82 | -------------------------------------------------------------------------------- /tests/test_py.py: -------------------------------------------------------------------------------- 1 | """Test the python distribution.""" 2 | 3 | import json 4 | import typing as t 5 | from pathlib import Path 6 | 7 | import jsonlogic_rs 8 | 9 | 10 | TEST_FILE = Path(__file__).parent / "data/tests.json" 11 | 12 | JsonValue = t.Union[dict, list, str, bool, None] 13 | 14 | 15 | class TestCase(t.NamedTuple): 16 | """A test case from the JSON.""" 17 | 18 | logic: JsonValue 19 | data: JsonValue 20 | exp: JsonValue 21 | 22 | 23 | def load_tests() -> t.List[TestCase]: 24 | """Load the test json into a series of cases.""" 25 | with open(TEST_FILE) as f: 26 | raw_cases = filter(lambda case: not isinstance(case, str), json.load(f)) 27 | return list(map(lambda case: TestCase(*case), raw_cases)) 28 | 29 | 30 | def run_tests() -> None: 31 | """Run through the tests and assert we get the right output.""" 32 | for idx, case in enumerate(load_tests()): 33 | result = jsonlogic_rs.apply(case.logic, case.data) 34 | assert result == case.exp, f"Failed test case {idx}: {case}" 35 | 36 | 37 | if __name__ == "__main__": 38 | run_tests() 39 | -------------------------------------------------------------------------------- /tests/test_py.rs: -------------------------------------------------------------------------------- 1 | //! Tests for the python bindings 2 | //! 3 | //! Note that Python 3.6+ must be installed for these tests to work. 4 | //! 5 | //! The actual tests are found in `test_py.py`. This file just serves 6 | //! as a runner. 7 | 8 | #[cfg(feature = "python")] 9 | use std::process::Command; 10 | 11 | #[cfg(feature = "python")] 12 | #[test] 13 | fn test_python_dist() { 14 | let py_build_res = Command::new("make") 15 | .arg("develop-py") 16 | .output() 17 | .expect("Could not spawn make"); 18 | assert!(py_build_res.status.success(), "{:?}", py_build_res); 19 | 20 | let py_test_res = Command::new("make") 21 | .arg("test-py") 22 | .output() 23 | .expect("Could not spawn make"); 24 | assert!(py_test_res.status.success(), "{:?}", py_test_res); 25 | } 26 | -------------------------------------------------------------------------------- /tests/test_wasm.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test the WASM package using node 3 | */ 4 | 5 | const fs = require("fs"); 6 | const path = require("path"); 7 | const jsonlogic = require("../js"); 8 | 9 | const load_test_json = () => { 10 | let data = fs.readFileSync( 11 | path.join(__dirname, "data/tests.json"), { encoding: "utf-8" } 12 | ); 13 | return JSON.parse(data); 14 | }; 15 | 16 | const print_case = (c, res) => { 17 | console.log(` Logic: ${JSON.stringify(c[0])}`); 18 | console.log(` Data: ${JSON.stringify(c[1])}`); 19 | console.log(` Expected: ${JSON.stringify(c[2])}`); 20 | console.log(` Actual: ${res && JSON.stringify(res)}`); 21 | } 22 | 23 | const run_tests = (cases) => { 24 | const no_comments = cases.filter(i => typeof i !== "string"); 25 | for (c of no_comments) { 26 | const logic = c[0]; 27 | const data = c[1]; 28 | const exp = c[2]; 29 | 30 | let res; 31 | try { 32 | res = jsonlogic.apply(logic, data); 33 | // res = jsonlogic.apply("apple", {}); 34 | } 35 | catch (e) { 36 | console.log("Test errored!"); 37 | console.log(` Error: ${e}}`); 38 | print_case(c); 39 | process.exit(2); 40 | } 41 | 42 | if (JSON.stringify(res) !== JSON.stringify(exp)) { 43 | console.log("Failed Test!") 44 | print_case(c, res) 45 | process.exit(1); 46 | } 47 | } 48 | }; 49 | 50 | const main = () => { 51 | run_tests(load_test_json()); 52 | }; 53 | 54 | main(); 55 | -------------------------------------------------------------------------------- /tests/test_wasm.rs: -------------------------------------------------------------------------------- 1 | //! Tests for the WASM target 2 | //! 3 | //! Note that a relatively recent version of node needs to be available 4 | //! for these tests to run. 5 | //! 6 | //! These tests will only run if the "wasm" feature is active. 7 | //! 8 | //! Note that the actual tests are found in `test_wasm.js`. This file 9 | //! just serves as a runner. 10 | 11 | #[cfg(feature = "wasm")] 12 | use std::process::Command; 13 | 14 | #[cfg(feature = "wasm")] 15 | #[test] 16 | fn test_node_pkg() { 17 | let build_res = Command::new("make") 18 | .arg("debug-wasm") 19 | .output() 20 | .expect("Could not spawn make"); 21 | assert!(build_res.status.success(), "{:?}", build_res); 22 | 23 | let test_res = Command::new("make") 24 | .arg("test-wasm") 25 | .output() 26 | .expect("Could not spawn make"); 27 | assert!(test_res.status.success(), "{:?}", test_res); 28 | } 29 | --------------------------------------------------------------------------------