diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f232465..9377f01 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,26 +1,15 @@ version: 2 updates: - # Enable version updates for Cargo (main crate) + # Enable version updates for Cargo (main crate and fuzz crate) - package-ecosystem: "cargo" - directory: "/" - schedule: - interval: "monthly" - open-pull-requests-limit: 10 - labels: - - "dependencies" - commit-message: - prefix: "deps" - include: "scope" - - # Enable version updates for Cargo (fuzz crate) - - package-ecosystem: "cargo" - directory: "/fuzz" + directories: + - "/" + - "/fuzz" schedule: interval: "monthly" open-pull-requests-limit: 10 labels: - "dependencies" - - "fuzzing" commit-message: prefix: "deps" include: "scope" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 88fd754..5c74d02 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,34 +8,52 @@ on: env: CARGO_TERM_COLOR: always + MSRV: '1.90' jobs: test: name: Test Suite runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - rust-version: ['1.90', 'stable'] - features: - - '' - - 'scheme' - - 'jsonlogic' - - 'scheme,jsonlogic' + rust-version: ['msrv', 'stable'] + cargo-args: + - '--no-default-features' + - '--no-default-features --features scheme' + - '--no-default-features --features jsonlogic' + - '--no-default-features --features scheme,jsonlogic' steps: - name: Checkout code uses: actions/checkout@v6 + - name: Resolve toolchain + id: resolve + shell: bash + run: | + rust="${{ matrix.rust-version }}" + [[ "$rust" == "msrv" ]] && rust="$MSRV" + echo "toolchain=$rust" >> "$GITHUB_OUTPUT" + - name: Install Rust uses: dtolnay/rust-toolchain@master with: - toolchain: ${{ matrix.rust-version }} + toolchain: ${{ steps.resolve.outputs.toolchain }} + + - name: Install clippy (stable only) + if: matrix.rust-version == 'stable' + run: rustup component add clippy - name: Cache dependencies uses: Swatinem/rust-cache@v2 - name: Run tests - run: cargo test --features="${{ matrix.features }}" --verbose + run: cargo test --all-targets ${{ matrix.cargo-args }} --verbose + + - name: Run clippy + if: matrix.rust-version == 'stable' + run: cargo clippy --all-targets ${{ matrix.cargo-args }} -- -D warnings - name: Run doc tests (all features) run: cargo test --doc --all-features @@ -60,7 +78,9 @@ jobs: uses: taiki-e/install-action@cargo-llvm-cov - name: Generate code coverage - run: cargo llvm-cov --all-features --workspace --html --output-dir coverage-html + run: | + cargo llvm-cov --all-features --workspace --html --output-dir coverage-html + cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info --no-run - name: Upload coverage HTML uses: actions/upload-artifact@v7 @@ -69,8 +89,16 @@ jobs: path: coverage-html/ retention-days: 30 - lint: - name: Linting + - name: Upload to Codecov + uses: codecov/codecov-action@v5 + with: + files: lcov.info + fail_ci_if_error: true + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + fmt: + name: Formatting runs-on: ubuntu-latest steps: @@ -80,7 +108,7 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@stable with: - components: rustfmt, clippy + components: rustfmt - name: Cache dependencies uses: Swatinem/rust-cache@v2 @@ -88,9 +116,6 @@ jobs: - name: Check formatting run: cargo fmt --all -- --check - - name: Run clippy - run: cargo clippy --all-targets --all-features -- -D warnings - docs: name: Documentation runs-on: ubuntu-latest @@ -142,7 +167,31 @@ jobs: uses: actions/checkout@v6 - name: Install cargo-audit - run: cargo install cargo-audit + uses: taiki-e/install-action@v2 + with: + tool: cargo-audit - name: Run security audit run: cargo audit + + minimal-versions: + name: Minimal Versions + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Install Rust nightly + uses: dtolnay/rust-toolchain@nightly + + - name: Install Rust stable + uses: dtolnay/rust-toolchain@stable + + - name: Install cargo-hack and cargo-minimal-versions + uses: taiki-e/install-action@v2 + with: + tool: cargo-hack,cargo-minimal-versions + + - name: Check minimal versions + run: cargo +nightly minimal-versions check --direct --all-features diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 25af35d..d945a1b 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -18,6 +18,9 @@ jobs: fuzz: name: Fuzz Testing runs-on: ubuntu-latest + permissions: + contents: read # to checkout the repo + issues: write # to create/comment on issues # Only run fuzzing for non-fork repositories to save folks CI credits. if: github.event.repository.fork == false strategy: @@ -38,6 +41,18 @@ jobs: - name: Cache dependencies uses: Swatinem/rust-cache@v2 + # Corpus caching: + # - Save key uses run_id so each run creates a new cache (caches are immutable) + # - Restore key uses prefix match to find the most recent cache from any prior run + # - Daily runs ensure we always have a recent corpus to build on + - name: Restore corpus cache + uses: actions/cache@v4 + with: + path: fuzz/corpus/${{ matrix.target }} + key: fuzz-corpus-${{ matrix.target }}-${{ github.run_id }} + restore-keys: | + fuzz-corpus-${{ matrix.target }}- + - name: Run fuzzer run: | DURATION=${{ github.event.inputs.duration || '300' }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 34c5c28..1c1e4ec 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: # so tags in forks don't create upstream-style releases. if: github.event.repository.fork == false outputs: - upload_url: ${{ steps.create_release.outputs.upload_url }} + tag_name: ${{ steps.tag_name.outputs.TAG_NAME }} steps: - name: Checkout code @@ -29,6 +29,14 @@ jobs: id: tag_name run: echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + - name: Verify tag is on main + run: | + git fetch origin main + if ! git merge-base --is-ancestor $GITHUB_SHA origin/main; then + echo "Error: Tag must point to a commit on the main branch" + exit 1 + fi + - name: Verify version matches tag run: | TAG_VERSION=${GITHUB_REF#refs/tags/v} @@ -40,15 +48,9 @@ jobs: echo "Version check passed: $CARGO_VERSION" - name: Create Release - id: create_release - uses: actions/create-release@v1 env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ steps.tag_name.outputs.TAG_NAME }} - release_name: Release ${{ steps.tag_name.outputs.TAG_NAME }} - draft: false - prerelease: false + GH_TOKEN: ${{ github.token }} + run: gh release create ${{ steps.tag_name.outputs.TAG_NAME }} --title "Release ${{ steps.tag_name.outputs.TAG_NAME }}" --generate-notes publish-crate: name: Publish to crates.io @@ -56,6 +58,9 @@ jobs: # crates.io publishing is only meaningful for non-fork roots if: github.event.repository.fork == false needs: create-release + environment: crates-io + permissions: + id-token: write # Required for OIDC trusted publishing steps: - name: Checkout code @@ -67,8 +72,14 @@ jobs: - name: Cache dependencies uses: Swatinem/rust-cache@v2 + - name: Authenticate with crates.io + uses: rust-lang/crates-io-auth-action@v1 + id: auth + - name: Publish to crates.io - run: cargo publish --all-features --token ${{ secrets.CRATES_IO_TOKEN }} + run: cargo publish --all-features + env: + CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} build-binaries: name: Build Release Binaries @@ -126,11 +137,6 @@ jobs: tar -czf ${{ matrix.archive_name }} -C staging . - name: Upload Release Asset - uses: actions/upload-release-asset@v1 env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.create-release.outputs.upload_url }} - asset_path: ./${{ matrix.archive_name }} - asset_name: ${{ matrix.archive_name }} - asset_content_type: application/octet-stream + GH_TOKEN: ${{ github.token }} + run: gh release upload ${{ needs.create-release.outputs.tag_name }} ./${{ matrix.archive_name }} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..98b3b22 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,86 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## 0.3.1 + +### Added + +- Code coverage reporting in CI with Codecov integration +- Maintainer's guide (`docs/MAINTAINERS-GUIDE.md`) +- README facelift + +### Changed + +- Release process switched to OIDC +- Improved CI pipeline performance with cached tool binaries +- Simplified dependabot configuration +- Fuzzing runs carry over corpus data between runs +- Dependency updates +- Check for cargo minimum dependency versions + +### Fixed + +- Code coverage improved from 93% to 97% + +## 0.3.0 + +### Added + +- Performance benchmarks in CI with criterion with regression detection + +### Changed + +- **Breaking:** Simplified project module hierarchy +- Benchmark uploads and comments restricted to main branch and PRs +- Clippy clean when not all features are enabled +- Updated dependencies + +## 0.2.1 + +### Added + +- Type-safe custom operations registration API (`register_builtin_operation`, `register_variadic_builtin_operation`) +- Iterator-based signatures for list and variadic "rest" parameters +- Project fuzzing support with cargo-fuzz for both S-expression and JSONLogic parsing/evaluation +- Daily fuzzing CI workflow +- Release binary packaging as zip/tar.gz +- Release version verification against git tag +- `.gitattributes` for standardized newline handling + +### Changed + +- Builtin functions migrated to new type-safe mechanics +- Eliminated `.expect()` calls by construction + +### Fixed + +- Disabled release tasks on forks + +## 0.1.2 + +### Added + +- Official JSONLogic.com conformance test suite +- GitHub CI pipeline +- Dependabot configuration + +## 0.1.1 + +### Changed + +- Added docs.rs metadata to build documentation with all features enabled + +## 0.1.0 + +### Added + +- Mini Scheme interpreter with integer-only arithmetic and strict typing/booleans +- JSONLogic interpreter with integer-only arithmetic, no nulls, and strict typing/booleans +- Precompiled operations, parse-time arity validation, and stack overflow guards +- Registration of new builtins. +- Sample REPL + diff --git a/Cargo.lock b/Cargo.lock index 6906833..ac85aec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,15 +40,15 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "cast" @@ -58,9 +58,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.51" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", "shlex", @@ -107,18 +107,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.53" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.53" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" dependencies = [ "anstyle", "clap_lex", @@ -126,9 +126,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.6" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "clipboard-win" @@ -246,9 +246,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.6" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "half" @@ -281,15 +281,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -297,27 +297,27 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.177" +version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "nibble_vec" @@ -410,18 +410,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.42" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] @@ -458,9 +458,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -470,9 +470,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -481,13 +481,13 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "rulesxp" -version = "0.3.0" +version = "0.3.1" dependencies = [ "criterion", "nom", @@ -497,9 +497,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", @@ -602,9 +602,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "syn" -version = "2.0.111" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -623,9 +623,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" @@ -657,9 +657,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -670,9 +670,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -680,9 +680,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", @@ -693,18 +693,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.83" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -905,18 +905,18 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "zerocopy" -version = "0.8.31" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.31" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" dependencies = [ "proc-macro2", "quote", @@ -925,6 +925,6 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.6" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aac060176f7020d62c3bcc1cdbcec619d54f48b07ad1963a3f80ce7a0c17755f" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index bc3f0e0..c2c2d16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rulesxp" -version = "0.3.0" +version = "0.3.1" edition = "2024" rust-version = "1.90" description = "Multi-language rules expression evaluator supporting JSONLogic and Scheme with strict typing" @@ -25,7 +25,7 @@ required-features = ["scheme", "jsonlogic"] [dependencies] nom = "8.0" -serde_json = { version = "1.0", optional = true } +serde_json = { version = "1.0.25", optional = true } [dev-dependencies] rustyline = "17.0" diff --git a/README.md b/README.md index bdbff45..de29b0c 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,23 @@ -# RulesXP +# ![RulesXP](https://raw.githubusercontent.com/microsoft/rulesxp/main/docs/rulesxp-logo.svg)   [![CI]][actions] [![Fuzzing]][fuzz] [![codecov]][codecov-link] [![Crates.io]][crates.io] [![Documentation]][docs.rs] -[![CI](https://github.com/microsoft/rulesxp/workflows/CI/badge.svg)](https://github.com/microsoft/rulesxp/actions/workflows/ci.yml) -[![Crates.io](https://img.shields.io/crates/v/rulesxp.svg)](https://crates.io/crates/rulesxp) -[![Documentation](https://docs.rs/rulesxp/badge.svg)](https://docs.rs/rulesxp) - -**Multi-Language Rules Expression Evaluator** +## RulesXP: Multi-Language Rules Expression Evaluator RulesXP is a minimalistic expression evaluator that supports both JSONLogic and Scheme syntax with strict typing. It's designed for reliable rule evaluation with predictable behavior. **Note that this project is a work in progress and the API and feature set are expected to change** +[CI]: https://github.com/microsoft/rulesxp/workflows/CI/badge.svg +[actions]: https://github.com/microsoft/rulesxp/actions/workflows/ci.yml +[Fuzzing]: https://github.com/microsoft/rulesxp/actions/workflows/fuzz.yml/badge.svg +[fuzz]: https://github.com/microsoft/rulesxp/actions/workflows/fuzz.yml +[codecov]: https://codecov.io/gh/microsoft/rulesxp/graph/badge.svg +[codecov-link]: https://codecov.io/gh/microsoft/rulesxp +[Crates.io]: https://img.shields.io/crates/v/rulesxp.svg +[crates.io]: https://crates.io/crates/rulesxp +[Documentation]: https://docs.rs/rulesxp/badge.svg +[docs.rs]: https://docs.rs/rulesxp + ## Features ### Dual Language Support diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 0000000..59cb72d --- /dev/null +++ b/clippy.toml @@ -0,0 +1,2 @@ +allow-expect-in-tests = true +allow-unwrap-in-tests = true diff --git a/docs/MAINTAINERS-GUIDE.md b/docs/MAINTAINERS-GUIDE.md new file mode 100644 index 0000000..f21581c --- /dev/null +++ b/docs/MAINTAINERS-GUIDE.md @@ -0,0 +1,244 @@ +# Maintainers Guide + +## Issues and Pull Requests + +Please triage incoming issues and review pull requests in a timely manner. Dependabot PRs, +community contributions, and bug reports all benefit from prompt attention — stale PRs discourage +contributors and stale issues accumulate. Aim to provide an initial response within a few business +days. + +--- + +## Dependency Management + +### Dependabot + +[Dependabot](https://docs.github.com/en/code-security/dependabot) is configured in +[.github/dependabot.yml](../.github/dependabot.yml) to open pull requests **monthly** for: + +- **Cargo dependencies** in `/` (the main crate) +- **Cargo dependencies** in `/fuzz` (the fuzz crate) +- **GitHub Actions** versions in workflows + +PRs are labeled `dependencies` and prefixed with `deps` (or `ci` for Actions updates). + +**Important limitation:** Dependabot only updates *direct* dependencies that have newer versions +available. It does **not** update transitive (indirect) dependencies in `Cargo.lock`. Over time the +lock file can drift, accumulating outdated transitive dependencies that Dependabot never touches. + +Dependabot runs are typically fast (a few minutes) because it only checks the manifest files for +newer versions and opens PRs — it does not build or test the crate itself. The CI workflows +triggered by those PRs handle building and testing. + +### Updating Dependencies Locally + +To keep **all** dependencies current — including transitive ones that Dependabot misses — +periodically run the following locally. + +#### 1. Update `Cargo.lock` (transitive dependencies) + +```sh +cargo update +``` + +This resolves every dependency (direct and transitive) to the latest SemVer-compatible version and +rewrites `Cargo.lock`. It does **not** change version requirements in `Cargo.toml`, so it is always +safe to run. Review the diff, then build and test: + +> **Note:** `Cargo.lock` is ignored by downstream consumers of a library crate — they resolve their +> own dependency versions. We check it in anyway so that our own example and test builds are +> deterministic. Keeping it fresh with `cargo update` is still valuable to ensure we're testing +> against current transitive dependencies. The version requirements in `Cargo.toml`, on the other +> hand, *are* inherited by callers and should be kept accurate. + +```sh +cargo build --all-features --all-targets +cargo test --all-features --all-targets +cargo clippy --all-features --all-targets -- -D warnings +``` + +#### 2. Upgrade direct dependency versions (`Cargo.toml`) + +For bumping the version *requirements* in `Cargo.toml` (e.g., `nom = "7.0"` → `nom = "8.0"`), use +[`cargo-edit`](https://github.com/killercup/cargo-edit): + +```sh +# Install once +cargo install cargo-edit + +# Interactively review and choose which direct dependencies to upgrade +cargo upgrade -i +``` + +> **Note:** Cargo itself is gaining a native `cargo update --breaking` command, but as of this +> writing it is still unstable and requires nightly +> (`cargo +nightly -Zunstable-options update --breaking`). Once stabilized, `cargo-edit` will no +> longer be needed. + +`cargo upgrade -i` shows each available upgrade and lets you accept or skip it. After upgrading, +look for breakage: + +```sh +cargo build --all-features --all-targets +cargo test --all-features --all-targets +cargo clippy --all-features --all-targets -- -D warnings +``` + +Fix any compilation errors or API changes before committing. Major-version bumps (e.g., `nom` +7.x → 8.x) are the most likely to require code changes. Before accepting an upgrade, check the +**CHANGELOG** or release notes of the updated crate, or review the commit history between the old +and new versions, to make a judgement call on how to adapt to upgrade-mandated changes and what +migration steps may be needed. + +> **Note:** Always pass `--all-features --all-targets` to build, test, and clippy commands. +> `--all-features` enables optional features (e.g., `scheme`, `jsonlogic`) so that all code paths +> are compiled and checked. `--all-targets` includes examples, benchmarks, and tests — not just the +> library — ensuring nothing is missed. + +#### Recommended workflow + +1. `cargo update` — pick up all compatible transitive updates. +2. `cargo upgrade -i` — review and accept direct dependency bumps. +3. Build, test, and lint. +4. Commit `Cargo.toml` and `Cargo.lock` together. + +--- + +## Publishing a New Version to crates.io + +The crate is published automatically by the [release workflow](../.github/workflows/release.yml) +when a version tag is pushed. The workflow verifies that the tag matches the version in +`Cargo.toml`, publishes to crates.io, creates a GitHub Release, and uploads platform binaries. + +Only members of the [FIT: Update Tools](https://github.com/orgs/microsoft/teams/fit-update-tools) +team can create `v*` tags (enforced by a +[tag ruleset](https://github.com/microsoft/rulesxp/settings/rules)), so publishing is limited to +this group. + +### Step-by-step + +1. **Decide the new version number.** Follow + [Semantic Versioning (SemVer)](https://semver.org/) and Cargo's interpretation of it: + + **Pre-1.0** (current — `0.x.y`): the **minor** component signals breaking changes: + - **Patch** (`0.3.0` → `0.3.1`) — backward-compatible bug fixes or internal changes. + - **Minor** (`0.3.0` → `0.4.0`) — **breaking** API changes (or significant new + functionality). + + **Post-1.0** (`≥ 1.0.0`): follows conventional SemVer: + - **Patch** (`1.0.0` → `1.0.1`) — backward-compatible bug fixes. + - **Minor** (`1.0.0` → `1.1.0`) — backward-compatible new functionality. + - **Major** (`1.0.0` → `2.0.0`) — breaking API changes. + +2. **Update `Cargo.toml`:** + + ```toml + [package] + version = "0.4.0" # ← new version + ``` + +3. **Commit and push to `main`:** + + ```sh + git add Cargo.toml Cargo.lock + git commit -m "release: v0.4.0" + git push origin main + ``` + +4. **Create and push an annotated tag:** + + ```sh + git tag -a v0.4.0 -m "v0.4.0" + git push origin v0.4.0 + ``` + + > **Note:** A [tag ruleset](https://github.com/microsoft/rulesxp/settings/rules) restricts + > creation and deletion of `v*` tags to members of the **FIT: Update Tools** team. If you get a + > permission error when pushing a tag, verify your team membership. + + The release workflow will: + - Verify the tag version matches `Cargo.toml`. + - Publish to crates.io via OIDC trusted publishing (no token required). + - Create a GitHub Release with the tag. + - Build and attach platform binaries (Linux x64, Windows x64, macOS ARM64). + +5. **Verify** the release on [crates.io/crates/rulesxp](https://crates.io/crates/rulesxp) and + the [GitHub Releases](https://github.com/microsoft/rulesxp/releases) page. + +### Trusted Publishing (OIDC) + +The release workflow authenticates to crates.io using +[trusted publishing](https://crates.io/docs/trusted-publishing) — an +OIDC-based mechanism where crates.io trusts identity assertions from GitHub Actions. No API tokens +or repository secrets are needed. + +**How it works:** When the `publish-crate` job runs, GitHub Actions issues a short-lived JWT signed +by GitHub's OIDC provider. `cargo publish` sends this JWT to crates.io, which verifies the +signature and checks that the token's claims (repository, workflow, environment) match the trusted +publisher configuration on file for the crate. If everything matches, the publish is allowed. + +**Setup / reconfiguration:** + +If the trusted publisher ever needs to be re-configured (e.g., after a repo transfer or workflow +rename): + +1. Sign in to [crates.io](https://crates.io) as a crate owner. +2. Go to the [rulesxp crate settings](https://crates.io/crates/rulesxp/settings). +3. Under **Trusted Publishers**, add or update the GitHub configuration: + - **Repository owner:** `microsoft` + - **Repository name:** `rulesxp` + - **Workflow filename:** `release.yml` + - **Environment:** `crates-io` +4. On the GitHub side, ensure the `crates-io` + [deployment environment](https://github.com/microsoft/rulesxp/settings/environments) exists. + Optionally add protection rules (e.g., required reviewers) for an extra approval gate before + publishing. + +--- + +## Code Coverage (Codecov) + +The CI workflow generates code coverage using +[`cargo-llvm-cov`](https://github.com/taiki-e/cargo-llvm-cov) and uploads the results to +[Codecov](https://codecov.io/gh/microsoft/rulesxp). A coverage badge is displayed in the README. + +### How it works + +The `coverage` job in [ci.yml](../.github/workflows/ci.yml): + +1. Runs `cargo llvm-cov --all-features --workspace` to produce both an HTML report (uploaded as a + build artifact) and an LCOV file. +2. Uploads the LCOV file to Codecov using + [`codecov/codecov-action@v5`](https://github.com/codecov/codecov-action). + +### Setup / Onboarding + +If the Codecov integration ever needs to be re-configured (e.g., after a repo transfer): + +1. Sign in to [codecov.io](https://codecov.io) with a GitHub account that has admin access to the + repository. +2. Enable the `microsoft/rulesxp` repository in the Codecov dashboard. +3. Copy the **upload token** from the Codecov repository settings page. +4. Add (or update) the `CODECOV_TOKEN` repository secret in GitHub: + **Settings → Secrets and variables → Actions → `CODECOV_TOKEN`**. + +### Refreshing the Codecov Token + +Codecov upload tokens do not expire by default, but if the token is revoked or rotated in the +Codecov dashboard: + +1. Go to the [Codecov settings for rulesxp](https://codecov.io/gh/microsoft/rulesxp/settings). +2. Copy the new upload token. +3. Update the `CODECOV_TOKEN` secret in the + [repository settings](https://github.com/microsoft/rulesxp/settings/secrets/actions). + +### Viewing Coverage + +- **Codecov dashboard:** +- **HTML artifact:** Download the `coverage-report` artifact from any CI run on the + [Actions page](https://github.com/microsoft/rulesxp/actions/workflows/ci.yml). +- **Locally:** + + ```sh + cargo llvm-cov --all-features --workspace --html --open + ``` diff --git a/docs/rulesxp-logo.svg b/docs/rulesxp-logo.svg new file mode 100644 index 0000000..aba030d --- /dev/null +++ b/docs/rulesxp-logo.svg @@ -0,0 +1,24 @@ + + RulesXP + + + + + + + + + + + + + + + + + + + diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index fee4a84..a05496e 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -10,9 +10,9 @@ checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" [[package]] name = "cc" -version = "1.2.41" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", "jobserver", @@ -28,9 +28,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "find-msvc-tools" -version = "0.1.4" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "getrandom" @@ -46,9 +46,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jobserver" @@ -62,9 +62,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.177" +version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "libfuzzer-sys" @@ -78,9 +78,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "nom" @@ -93,18 +93,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.41" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] @@ -131,12 +131,6 @@ dependencies = [ "rulesxp", ] -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - [[package]] name = "serde" version = "1.0.228" @@ -168,15 +162,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -187,9 +181,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "syn" -version = "2.0.108" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -198,21 +192,27 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.20" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ "wit-bindgen", ] [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "zmij" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/src/ast.rs b/src/ast.rs index 303c15d..19029a0 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -415,6 +415,11 @@ mod helper_function_tests { Value::Bool(true), ]), ), + // From<&[T]> slice conversion + ( + Value::from(&[1i64, 2, 3][..]), + Value::List(vec![Value::Number(1), Value::Number(2), Value::Number(3)]), + ), ]; run_helper_function_tests(test_cases); @@ -441,4 +446,75 @@ mod helper_function_tests { assert_ne!(unspec, Value::Unspecified); assert_ne!(unspec, val(42)); } + + #[test] + #[cfg(feature = "scheme")] + fn test_debug_display_and_equality() { + use crate::evaluator::{create_global_env, eval}; + use crate::scheme::parse_scheme; + + let mut env = create_global_env(); + eval(&parse_scheme("(define f +)").unwrap(), &mut env).unwrap(); + let builtin_fn = eval(&parse_scheme("f").unwrap(), &mut env).unwrap(); + let func = eval(&parse_scheme("(lambda (x) (+ x 1))").unwrap(), &mut env).unwrap(); + let precompiled = parse_scheme("(+ 1 2)").unwrap(); + + let list_val = val([1, 2, 3]); + + // Debug: (value, expected_substring) + let debug_cases: Vec<(&Value, &str)> = vec![ + (&list_val, "List("), + (&precompiled, "PrecompiledOp"), + (&builtin_fn, "BuiltinFunction"), + (&func, "Function"), + (&Value::Unspecified, "Unspecified"), + ]; + for (value, expected) in &debug_cases { + assert!( + format!("{value:?}").contains(expected), + "Debug of {value:?} should contain '{expected}'" + ); + } + + // Display: (value, expected_substring) + let display_cases: Vec<(&Value, &str)> = vec![ + (&builtin_fn, "#"), + (&Value::Unspecified, "#"), + ]; + for (value, expected) in &display_cases { + assert!( + format!("{value}").contains(expected), + "Display of {value} should contain '{expected}'" + ); + } + + // PartialEq: one test per uncovered match arm + let mut env2 = create_global_env(); + eval(&parse_scheme("(define f2 +)").unwrap(), &mut env2).unwrap(); + assert!(builtin_fn == eval(&parse_scheme("f2").unwrap(), &mut env2).unwrap()); + + let func_b = eval(&parse_scheme("(lambda (x) (+ x 1))").unwrap(), &mut env).unwrap(); + assert!(func == func_b); + + assert!(precompiled == parse_scheme("(+ 1 2)").unwrap()); + + assert!(builtin_fn != func); // cross-variant catch-all arm + + // Error Display: (error, expected_substring) + let error_cases: Vec<(crate::Error, &str)> = vec![ + ( + crate::Error::arity_error_with_expr(2, 3, "x".into()), + "Arity error", + ), + (crate::Error::ParseError("x".into()), "Parse error"), + (crate::Error::arity_error(2, 3), "Arity error"), + ]; + for (error, expected) in &error_cases { + assert!( + format!("{error}").contains(expected), + "Error '{error}' should contain '{expected}'" + ); + } + } } diff --git a/src/builtinops.rs b/src/builtinops.rs index e4eb19a..a237a55 100644 --- a/src/builtinops.rs +++ b/src/builtinops.rs @@ -564,7 +564,6 @@ pub(crate) fn get_list_op() -> &'static BuiltinOp { } #[cfg(test)] -#[expect(clippy::unwrap_used)] // test code OK mod tests { use super::*; use crate::ast::{nil, val}; @@ -663,6 +662,18 @@ mod tests { .scheme_id, "=" ); + + // === Cover BuiltinOp/OpKind Debug And PartialEq === + let add_ref = find_scheme_op("+").unwrap(); + assert!(format!("{:?}", add_ref.op_kind).contains("Function")); + let if_ref = find_scheme_op("if").unwrap(); + assert!(format!("{:?}", if_ref.op_kind).contains("SpecialForm")); + assert!( + (add_ref.op_kind == add_ref.op_kind) + && (if_ref.op_kind == if_ref.op_kind) + && (add_ref.op_kind != if_ref.op_kind) + && (*add_ref == add_ref.clone()) + ); } /// Macro to create test cases, invoking builtins via the registry. diff --git a/src/evaluator.rs b/src/evaluator.rs index 1ea43f2..374d05d 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -562,13 +562,13 @@ pub fn create_global_env() -> Environment { env } -#[cfg(all(test, feature = "scheme"))] -#[expect(clippy::unwrap_used)] // test code OK +#[cfg(test)] +#[cfg(feature = "scheme")] mod tests { use super::*; use crate::Error; use crate::ast::{nil, sym, val}; - use crate::evaluator::{NumIter, ValueIter}; + use crate::evaluator::{BoolIter, NumIter, StringIter, ValueIter}; use crate::scheme::parse_scheme; /// Test result variants for comprehensive testing @@ -733,15 +733,34 @@ mod tests { Ok(nums.sum::()) } + fn string_length(s: &str) -> i64 { + s.len() as i64 + } + + fn all_true(mut bools: BoolIter<'_>) -> bool { + bools.all(|b| b) + } + + fn join_strings(strings: StringIter<'_>) -> String { + strings.collect::>().join(",") + } + let mut env = create_global_env(); // Fixed-arity and zero-arg builtins. env.register_builtin_operation::<(i64, i64)>("add2", add); env.register_builtin_operation::<()>("forty-two", forty_two); env.register_builtin_operation::<(i64, i64)>("safe-div", safe_div); + env.register_builtin_operation::<(&str,)>("str-len", string_length); // Iterator-based list and variadic builtins. env.register_builtin_operation::<(NumIter<'static>,)>("sum-list", sum_list); + env.register_builtin_operation::<(BoolIter<'static>,)>("all-true", all_true); + env.register_variadic_builtin_operation::<(StringIter<'static>,)>( + "join-strings", + Arity::AtLeast(0), + join_strings, + ); env.register_variadic_builtin_operation::<(ValueIter<'static>,)>( "first-rest-count", Arity::AtLeast(1), @@ -772,6 +791,7 @@ mod tests { // Fixed-arity and zero-arg builtins. ("(add2 7 5)", success(12)), ("(forty-two)", success(42)), + ("(forty-two 1)", ArityError), // 0-arg arity check should reject ("(safe-div 6 3)", success(2)), // Error case: division by zero surfaces as EvalError containing the message. ("(safe-div 1 0)", SpecificError("division by zero")), @@ -784,6 +804,14 @@ mod tests { // Explicit arity checking for variadic builtin. ("(sum-all-min1 1 2 3)", success(6)), ("(sum-all-min1)", ArityError), + // &str, BoolIter, StringIter parameter types & validation in custom builtins. + ("(str-len \"hello\")", success(5)), + ("(str-len 42)", SpecificError("expected string")), + ("(str-len)", ArityError), + ("(all-true (list #t #t #t))", success(true)), + ("(all-true (list 1 2))", SpecificError("expected boolean")), + ("(join-strings \"a\" \"b\" \"c\")", success("a,b,c")), + ("(join-strings 1 2)", SpecificError("expected string")), // Dynamic higher-order use of a builtin comparison: pass `>` as a value. ("((lambda (op a b c) (op a b c)) > 9 6 2)", success(true)), ("((lambda (op a b c) (op a b c)) > 9 6 7)", success(false)), @@ -792,6 +820,17 @@ mod tests { run_tests_in_specific_environment(&mut env, test_cases); } + #[test] + fn test_get_all_bindings_with_parent() { + let mut parent = create_global_env(); + parent.define("parent-var".into(), val(1)); + let mut child = Environment::with_parent(parent); + child.define("child-var".into(), val(2)); + let bindings = child.get_all_bindings(); + assert!(bindings.iter().any(|(n, _)| n == "child-var")); + assert!(bindings.iter().any(|(n, _)| n == "parent-var")); + } + #[test] #[expect(clippy::too_many_lines)] // Comprehensive test coverage is intentionally thorough fn test_comprehensive_operations_data_driven() { @@ -1068,6 +1107,12 @@ mod tests { ("(set! x 42)", SpecificError("Unbound variable: set!")), // Unsupported special forms appear as unbound variables // Type errors ("(+ 1 \"hello\")", SpecificError("expected number")), + // TypeError has extra annotation inside lambda body + ( + "((lambda (x) (+ x \"hello\")) 1)", + SpecificError("In lambda"), + ), + ("(1 2 3)", SpecificError("Cannot apply non-function")), ]; run_comprehensive_tests(test_cases); diff --git a/src/jsonlogic.rs b/src/jsonlogic.rs index 0beccae..52031bf 100644 --- a/src/jsonlogic.rs +++ b/src/jsonlogic.rs @@ -360,8 +360,8 @@ fn ast_to_json_value_with_context( } } -#[cfg(all(test, feature = "scheme"))] -#[expect(clippy::unwrap_used)] // test code OK +#[cfg(test)] +#[cfg(feature = "scheme")] mod tests { use core::panic; @@ -706,6 +706,20 @@ mod tests { r#"["lambda", ["x"], ["*", "x", "x"]]"#, Identical(r#"(list "lambda" (list "x") (list "*" "x" "x"))"#), ), + // Quote with non-array operand (normalizes to array form on roundtrip) + ( + r#"{"scheme-quote": 42}"#, + SemanticallyEquivalent("(quote 42)"), + ), + // Quote with wrong number of array operands + ( + r#"{"scheme-quote": [1, 2]}"#, + SpecificError("quote requires one operand"), + ), + // Float number that isn't an integer + (r#"3.14"#, SpecificError("not integer")), + // Large number overflow + (r#"99999999999999999999"#, SpecificError("too large")), ]; run_data_driven_tests(&test_cases); @@ -766,6 +780,35 @@ mod tests { ); } + #[test] + fn test_ast_to_jsonlogic_error_paths() { + let mut env = create_global_env(); + eval(&parse_scheme("(define f +)").unwrap(), &mut env).unwrap(); + + // Values that cannot be converted to JSONLogic + let unconvertible: Vec<(&str, Value)> = vec![ + ( + "BuiltinFunction", + eval(&parse_scheme("f").unwrap(), &mut env).unwrap(), + ), + ( + "Function", + eval(&parse_scheme("(lambda (x) (+ x 1))").unwrap(), &mut env).unwrap(), + ), + ("Unspecified", Value::Unspecified), + ]; + for (label, val) in &unconvertible { + assert!( + ast_to_jsonlogic(val).is_err(), + "{label} should fail conversion" + ); + } + + // Non-symbol list converts to JSON array + let list_val = Value::List(vec![Value::Number(1), Value::Number(2)]); + assert_eq!(ast_to_jsonlogic(&list_val).unwrap(), "[1,2]"); + } + /// Helper function to test AST equivalence and roundtrip (shared by Identical and IdenticalWithEvalError) fn test_ast_equivalence_and_roundtrip( jsonlogic: &str, diff --git a/src/scheme.rs b/src/scheme.rs index 5e27ed0..fede57b 100644 --- a/src/scheme.rs +++ b/src/scheme.rs @@ -339,7 +339,6 @@ fn validate_arity_in_ast(value: &Value) -> Result<(), Error> { } #[cfg(test)] -#[expect(clippy::unwrap_used)] // test code OK mod tests { use super::*; use crate::ast::{nil, sym, val}; @@ -484,6 +483,7 @@ mod tests { ("#xff", success(255)), ("#x0", success(0)), ("#x12345", success(74565)), + ("#xFFFFFFFFFFFFFFFF", Error), // Hex overflow // Edge cases - large integer literals ("9223372036854775807", success(i64::MAX)), ("-9223372036854775808", success(i64::MIN)), diff --git a/tests/jsonlogic_conformance.rs b/tests/jsonlogic_conformance.rs index b29c9e2..1b93456 100644 --- a/tests/jsonlogic_conformance.rs +++ b/tests/jsonlogic_conformance.rs @@ -1,7 +1,6 @@ // serde-json needs this set to be able to parse the huge JSONLogic official test list #![recursion_limit = "1024"] #![cfg(feature = "jsonlogic")] -#![expect(clippy::unwrap_used)] // test code OK use rulesxp::evaluator; use rulesxp::jsonlogic::{ast_to_jsonlogic, parse_jsonlogic};