mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
Compare commits
26 Commits
regorus-v0
...
regorus-v0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b80ef2d015 | ||
|
|
7e3fc08a14 | ||
|
|
48982222c5 | ||
|
|
90757210bc | ||
|
|
7bc9a50a52 | ||
|
|
08f3007b5c | ||
|
|
863601c2d5 | ||
|
|
976c04be8a | ||
|
|
fbfed6b49c | ||
|
|
595f9d34d5 | ||
|
|
a8c0588426 | ||
|
|
0e053832db | ||
|
|
f51731e584 | ||
|
|
10f2caf0c0 | ||
|
|
22047287b4 | ||
|
|
3a86c83827 | ||
|
|
d3d5367fd4 | ||
|
|
f3d9652a73 | ||
|
|
bdb2aba596 | ||
|
|
8d282f1ffd | ||
|
|
7d32bd9377 | ||
|
|
53b990f97d | ||
|
|
13eb06e4be | ||
|
|
3b2e639918 | ||
|
|
a381c38a90 | ||
|
|
5044d54d18 |
@@ -1,14 +0,0 @@
|
||||
[build]
|
||||
#target = "x86_64-unknown-linux-musl"
|
||||
|
||||
# Flags to enable code-coverage for all builds.
|
||||
# These can be removed later.
|
||||
#rustflags = ["-Cinstrument-coverage"]
|
||||
incremental = true
|
||||
|
||||
[env]
|
||||
# Name of coverage instrumentation log file.
|
||||
LLVM_PROFILE_FILE="target/cargo-test-%p-%m.profraw"
|
||||
|
||||
[target.x86_64-unknown-linux-gnu]
|
||||
rustflags = ["-Cinstrument-coverage"]
|
||||
@@ -18,30 +18,27 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Add musl target
|
||||
run: rustup target add x86_64-unknown-linux-musl
|
||||
- name: Install musl-gcc
|
||||
run: sudo apt update && sudo apt install -y musl-tools
|
||||
- name: Format Check
|
||||
run: cargo fmt --check
|
||||
- name: Build
|
||||
run: cargo build --verbose
|
||||
- name: Build Tests
|
||||
run: cargo build --all-targets --verbose
|
||||
- name: Clippy
|
||||
run: cargo clippy --all-targets --no-deps -- -Dwarnings
|
||||
run: cargo build -r --verbose
|
||||
- name: Doc Tests
|
||||
run: cargo test -r --doc
|
||||
- name: Run tests
|
||||
run: cargo test -r --verbose
|
||||
- name: Build (MUSL)
|
||||
run: cargo build --verbose --all-targets --target x86_64-unknown-linux-musl
|
||||
- name: Run tests (MUSL)
|
||||
run: cargo test -r --verbose --target x86_64-unknown-linux-musl
|
||||
- name: Run tests (ACI)
|
||||
run: cargo test -r --test aci
|
||||
- name: Run tests (OPA Conformance)
|
||||
run: >-
|
||||
cargo test -r --test opa --features opa-testutil -- $(tr '\n' ' ' < tests/opa.passing)
|
||||
|
||||
# - name: Install wasm-pack
|
||||
# run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
|
||||
# - name: Run wasm binding tests
|
||||
# run: |
|
||||
# cd bindings/wasm
|
||||
# wasm-pack test --node -r
|
||||
- name: Build (MUSL)
|
||||
run: cargo build --verbose --all-targets --target x86_64-unknown-linux-musl
|
||||
- name: Run tests (MUSL)
|
||||
run: cargo test -r --verbose --target x86_64-unknown-linux-musl
|
||||
- name: Run tests (MUSL ACI)
|
||||
run: cargo test -r --test aci --target x86_64-unknown-linux-musl
|
||||
- name: Run tests (MUSL OPA Conformance)
|
||||
run: >-
|
||||
cargo test -r --test opa --features opa-testutil --target x86_64-unknown-linux-musl -- $(tr '\n' ' ' < tests/opa.passing)
|
||||
89
.github/workflows/publish-java.yml
vendored
Normal file
89
.github/workflows/publish-java.yml
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
name: publish-java
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build for ${{ matrix.target }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- target: x86_64-unknown-linux-gnu
|
||||
os: ubuntu-latest
|
||||
extension: so
|
||||
build_cmd: zigbuild
|
||||
glibc: "2.17"
|
||||
- target: aarch64-unknown-linux-gnu
|
||||
os: ubuntu-latest
|
||||
extension: so
|
||||
build_cmd: zigbuild
|
||||
glibc: "2.17"
|
||||
- target: x86_64-apple-darwin
|
||||
os: macos-latest
|
||||
extension: dylib
|
||||
- target: aarch64-apple-darwin
|
||||
os: macos-latest
|
||||
extension: dylib
|
||||
- target: x86_64-pc-windows-msvc
|
||||
os: windows-latest
|
||||
extension: dll
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 8
|
||||
distribution: "corretto"
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
- if: ${{ matrix.build_cmd == 'zigbuild' }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
- if: ${{ matrix.build_cmd == 'zigbuild' }}
|
||||
run: pip install cargo-zigbuild
|
||||
- run: cargo ${{ matrix.build_cmd || 'build' }} --release --target ${{ matrix.target }}${{ matrix.glibc && format('.{0}', matrix.glibc) || '' }} --manifest-path ./bindings/java/Cargo.toml
|
||||
- run: mkdir -p native/${{ matrix.target }}
|
||||
- run: mv target/${{ matrix.target }}/release/*.${{ matrix.extension }} ./native/${{ matrix.target }}/
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: native-libraries-${{ matrix.target }}
|
||||
path: native/
|
||||
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 8
|
||||
distribution: "corretto"
|
||||
server-id: ossrh
|
||||
server-username: MAVEN_USERNAME
|
||||
server-password: MAVEN_PASSWORD
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: native-libraries-*
|
||||
merge-multiple: true
|
||||
path: ./bindings/java/native/
|
||||
- run: mvn package
|
||||
working-directory: ./bindings/java
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: built-jars
|
||||
path: ./bindings/java/target/regorus-java-*.jar
|
||||
- run: mvn deploy
|
||||
working-directory: ./bindings/java
|
||||
env:
|
||||
MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }}
|
||||
MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }}
|
||||
54
.github/workflows/rust-clippy.yml
vendored
Normal file
54
.github/workflows/rust-clippy.yml
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
# rust-clippy is a tool that runs a bunch of lints to catch common
|
||||
# mistakes in your Rust code and help improve your Rust code.
|
||||
# More details at https://github.com/rust-lang/rust-clippy
|
||||
# and https://rust-lang.github.io/rust-clippy/
|
||||
|
||||
name: rust-clippy analyze
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ "main" ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
rust-clippy-analyze:
|
||||
name: Run rust-clippy analyzing
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@16499b5e05bf2e26879000db0c1d13f7e13fa3af #@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
components: clippy
|
||||
override: true
|
||||
|
||||
- name: Install required cargo
|
||||
run: cargo install clippy-sarif sarif-fmt
|
||||
|
||||
- name: Run rust-clippy
|
||||
run:
|
||||
cargo clippy
|
||||
--all-features
|
||||
--message-format=json | clippy-sarif | tee rust-clippy-results.sarif | sarif-fmt
|
||||
continue-on-error: true
|
||||
|
||||
- name: Upload analysis results to GitHub
|
||||
uses: github/codeql-action/upload-sarif@v1
|
||||
with:
|
||||
sarif_file: rust-clippy-results.sarif
|
||||
wait-for-processing: true
|
||||
32
CHANGELOG.md
32
CHANGELOG.md
@@ -6,6 +6,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.1.2](https://github.com/microsoft/regorus/compare/regorus-v0.1.1...regorus-v0.1.2) - 2024-03-22
|
||||
|
||||
### Other
|
||||
- Handle non simple refs in chained expressions ([#182](https://github.com/microsoft/regorus/pull/182))
|
||||
- Ability to gather print statements ([#179](https://github.com/microsoft/regorus/pull/179))
|
||||
- Top-down evaluation ([#177](https://github.com/microsoft/regorus/pull/177))
|
||||
- Make unary `-` operator OPA compatible. ([#175](https://github.com/microsoft/regorus/pull/175))
|
||||
- Don't use deprecated chrono `Duration` methods ([#173](https://github.com/microsoft/regorus/pull/173))
|
||||
- Propagate Undefined in object expressions ([#171](https://github.com/microsoft/regorus/pull/171))
|
||||
- Bump to OPA v0.62.0 ([#169](https://github.com/microsoft/regorus/pull/169))
|
||||
- Fix regression ([#164](https://github.com/microsoft/regorus/pull/164))
|
||||
- Separately keep track of whether rules have been evaluated or not ([#163](https://github.com/microsoft/regorus/pull/163))
|
||||
- Link Linux libraries against glibc 2.17 using `cargo-zigbuild` ([#158](https://github.com/microsoft/regorus/pull/158))
|
||||
|
||||
## [0.1.1](https://github.com/microsoft/regorus/compare/regorus-v0.1.0...regorus-v0.1.1) - 2024-02-23
|
||||
|
||||
### Other
|
||||
- Handle else block without body ([#155](https://github.com/microsoft/regorus/pull/155))
|
||||
- Ignore errors from builtin functions in non strict mode ([#154](https://github.com/microsoft/regorus/pull/154))
|
||||
- Java publishing ([#151](https://github.com/microsoft/regorus/pull/151))
|
||||
- Document coverage feature; Convenience query functions ([#152](https://github.com/microsoft/regorus/pull/152))
|
||||
- Policy Coverage ([#149](https://github.com/microsoft/regorus/pull/149))
|
||||
- Initial implementation of policy coverage ([#146](https://github.com/microsoft/regorus/pull/146))
|
||||
- Java bindings ([#147](https://github.com/microsoft/regorus/pull/147))
|
||||
- Preserve false in single-expression queries ([#145](https://github.com/microsoft/regorus/pull/145))
|
||||
- Create rust-clippy.yml ([#143](https://github.com/microsoft/regorus/pull/143))
|
||||
- `arc` feature to enable using Engine and other data structures from multiple threads ([#142](https://github.com/microsoft/regorus/pull/142))
|
||||
- genpolicy tweaks ([#141](https://github.com/microsoft/regorus/pull/141))
|
||||
- io.jwt.decode ([#140](https://github.com/microsoft/regorus/pull/140))
|
||||
- Use compact_rc ([#139](https://github.com/microsoft/regorus/pull/139))
|
||||
- Scripting tweaks ([#138](https://github.com/microsoft/regorus/pull/138))
|
||||
|
||||
## [0.1.0-alpha.3](https://github.com/microsoft/regorus/compare/regorus-v0.1.0-alpha.2...regorus-v0.1.0-alpha.3) - 2024-02-01
|
||||
|
||||
### Fixed
|
||||
|
||||
28
Cargo.toml
28
Cargo.toml
@@ -3,13 +3,14 @@
|
||||
members = [
|
||||
"bindings/ffi",
|
||||
"bindings/python",
|
||||
"bindings/wasm"
|
||||
"bindings/wasm",
|
||||
"bindings/java",
|
||||
]
|
||||
|
||||
[package]
|
||||
name = "regorus"
|
||||
description = "A fast, lightweight Rego (OPA policy language) interpreter"
|
||||
version = "0.1.0"
|
||||
version = "0.1.2"
|
||||
edition = "2021"
|
||||
license-file = "LICENSE"
|
||||
repository = "https://github.com/microsoft/regorus"
|
||||
@@ -17,16 +18,21 @@ keywords = ["interpreter", "opa", "policy-as-code", "rego"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
default = ["full-opa"]
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
default = ["full-opa", "arc"]
|
||||
|
||||
arc = ["scientific/arc"]
|
||||
base64 = ["dep:data-encoding"]
|
||||
base64url = ["dep:data-encoding"]
|
||||
coverage = []
|
||||
crypto = ["dep:constant_time_eq", "dep:hmac", "dep:hex", "dep:md-5", "dep:sha1", "dep:sha2"]
|
||||
deprecated = []
|
||||
hex = ["dep:data-encoding"]
|
||||
http = []
|
||||
jwt = []
|
||||
jwt = ["dep:jsonwebtoken", "dep:data-encoding"]
|
||||
glob = ["dep:wax"]
|
||||
graph = []
|
||||
jsonschema = ["dep:jsonschema"]
|
||||
@@ -40,6 +46,7 @@ yaml = ["serde_yaml"]
|
||||
full-opa = [
|
||||
"base64",
|
||||
"base64url",
|
||||
"coverage",
|
||||
"crypto",
|
||||
"deprecated",
|
||||
"glob",
|
||||
@@ -90,9 +97,12 @@ uuid = { version = "1.6.1", features = ["v4", "fast-rng"], optional = true }
|
||||
jsonschema = { version = "0.17.1", default-features = false, optional = true }
|
||||
chrono = { version = "0.4.31", optional = true }
|
||||
chrono-tz = { version = "0.8.5", optional = true }
|
||||
|
||||
compact-rc = "0.5.2"
|
||||
jsonwebtoken = { version = "9.2.0", optional = true }
|
||||
itertools = "0.12.1"
|
||||
|
||||
[dev-dependencies]
|
||||
cfg-if = "1.0.0"
|
||||
clap = { version = "4.4.7", features = ["derive"] }
|
||||
colored-diff = "0.2.3"
|
||||
serde_yaml = "0.9.16"
|
||||
@@ -115,3 +125,9 @@ required-features = ["full-opa"]
|
||||
name="aci"
|
||||
harness=false
|
||||
test=false
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
# To build locally:
|
||||
# RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features --no-deps
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
26
README.md
26
README.md
@@ -9,11 +9,11 @@
|
||||
Regorus is also
|
||||
- *cross-platform* - Written in platform-agnostic Rust.
|
||||
- *current* - We strive to keep Regorus up to date with latest OPA release. Regorus supports `import rego.v1`.
|
||||
- *compliant* - Regorus is mostly compliant with the latest [OPA release v0.61.0](https://github.com/open-policy-agent/opa/releases/tag/v0.61.0). See [OPA Conformance](#opa-conformance) for details. Note that while we behaviorally produce the same results, we don't yet support all the builtins.
|
||||
- *compliant* - Regorus is mostly compliant with the latest [OPA release v0.62.0](https://github.com/open-policy-agent/opa/releases/tag/v0.62.0). See [OPA Conformance](#opa-conformance) for details. Note that while we behaviorally produce the same results, we don't yet support all the builtins.
|
||||
- *extensible* - Extend the Rego language by implementing custom stateful builtins in Rust.
|
||||
See [add_extension](https://github.com/microsoft/regorus/blob/fc68bf9c8bea36427dae9401a7d1f6ada771f7ab/src/engine.rs#L352).
|
||||
Support for extensibility using other languages coming soon.
|
||||
- *polyglot* - In addition to Rust, Regorus can be used from *C*, *C++*, *C#*, *Golang*, *Javascript* and *Python*.
|
||||
- *polyglot* - In addition to Rust, Regorus can be used from *C*, *C++*, *C#*, *Golang*, *Java*, *Javascript* and *Python*.
|
||||
This is made possible by the excellent FFI tools available in the Rust ecosystem. See [bindings](#bindings) for information on how to use Regorus from different languages.
|
||||
|
||||
To try out a *Javascript(WASM)* compiled version of Regorus from your browser, visit [Regorus Playground](https://anakrish.github.io/regorus-playground/).
|
||||
@@ -85,6 +85,8 @@ Regorus can be used from a variety of languages:
|
||||
- *C#*: C# binding is generated using [csbindgen](https://github.com/Cysharp/csbindgen). See [bindings/csharp](https://github.com/microsoft/regorus/tree/main/bindings/csharp) for an example of how to build and use Regorus in your C# projects.
|
||||
- *Golang*: The C bindings are exposed to Golang via [CGo](https://pkg.go.dev/cmd/cgo). See [bindings/go](https://github.com/microsoft/regorus/tree/main/bindings/go) for an example of how to build and use Regorus in your Go projects.
|
||||
- *Python*: Python bindings are generated using [pyo3](https://github.com/PyO3/pyo3). Wheels are created using [maturin](https://github.com/PyO3/maturin). See [bindings/python](https://github.com/microsoft/regorus/tree/main/bindings/python).
|
||||
- *Java*: Java bindings are developed using [jni-rs](https://github.com/jni-rs/jni-rs).
|
||||
See [bindings/java](https://github.com/microsoft/regorus/tree/main/bindings/java).
|
||||
- *Javascript*: Regorus is compiled to WASM using [wasmpack](https://github.com/rustwasm/wasm-pack).
|
||||
See [bindings/wasm](https://github.com/microsoft/regorus/tree/main/bindings/wasm) for an example of using Regorus from nodejs.
|
||||
To try out a *Javascript(WASM)* compiled version of Regorus from your browser, visit [Regorus Playground](https://anakrish.github.io/regorus-playground/).
|
||||
@@ -149,7 +151,7 @@ This produces the following output
|
||||
}
|
||||
```
|
||||
|
||||
Next, evaluate a sample [policy](examples/example.rego) and [input](examples/input.json)
|
||||
Next, evaluate a sample [policy](https://github.com/microsoft/regorus/blob/main/examples/example.rego) and [input](https://github.com/microsoft/regorus/blob/main/examples/input.json)
|
||||
(borrowed from [Rego tutorial](https://www.openpolicyagent.org/docs/latest/#2-try-opa-eval)):
|
||||
|
||||
```bash
|
||||
@@ -162,6 +164,23 @@ Finally, evaluate real-world [policies](tests/aci/) used in Azure Container Inst
|
||||
$ regorus eval -b tests/aci -d tests/aci/data.json -i tests/aci/input.json data.policy.mount_overlay=x
|
||||
```
|
||||
|
||||
## Policy coverage
|
||||
|
||||
Regorus allows determining which lines of a policy have been executed using the `coverage` feature (enabled by default).
|
||||
|
||||
We can try it out using the `regorus` example program by passing in the `--coverage` flag.
|
||||
|
||||
```shell
|
||||
$ regorus eval -d examples/example.rego -i examples/input.json data.example --coverage
|
||||
```
|
||||
|
||||
It produces the following coverage report which shows that all lines are executed except the line that sets `allow` to true.
|
||||
|
||||

|
||||
|
||||
See [Engine::get_coverage_report](https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.get_coverage_report) for details.
|
||||
Policy coverage information is useful for debugging your policy as well as to write tests for your policy so that all
|
||||
lines of the policy are exercised by the tests.
|
||||
|
||||
## ACI Policies
|
||||
|
||||
@@ -248,7 +267,6 @@ The following test suites don't pass fully due to mising builtins:
|
||||
- `graphql`
|
||||
- `invalidkeyerror`
|
||||
- `jsonpatch`
|
||||
- `jwtbuiltins`
|
||||
- `jwtdecodeverify`
|
||||
- `jwtencodesign`
|
||||
- `jwtencodesignraw`
|
||||
|
||||
1
bindings/java/.gitignore
vendored
Normal file
1
bindings/java/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
target/
|
||||
13
bindings/java/CHANGELOG.md
Normal file
13
bindings/java/CHANGELOG.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# 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.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.1.0](https://github.com/microsoft/regorus/releases/tag/regorus-java-v0.1.0) - 2024-02-23
|
||||
|
||||
### Other
|
||||
- Java publishing ([#151](https://github.com/microsoft/regorus/pull/151))
|
||||
- Java bindings ([#147](https://github.com/microsoft/regorus/pull/147))
|
||||
17
bindings/java/Cargo.toml
Normal file
17
bindings/java/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "regorus-java"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/microsoft/regorus/bindings/java"
|
||||
description = "Java bindings for Regorus - a fast, lightweight Rego interpreter written in Rust"
|
||||
keywords = ["interpreter", "opa", "policy-as-code", "rego"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.79"
|
||||
serde_json = "1.0.112"
|
||||
jni = "0.21.1"
|
||||
regorus = { path = "../.." }
|
||||
97
bindings/java/README.md
Normal file
97
bindings/java/README.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# Regorus Java
|
||||
|
||||
**Regorus** is
|
||||
|
||||
- *Rego*-*Rus(t)* - A fast, light-weight [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/)
|
||||
interpreter written in Rust.
|
||||
- *Rigorous* - A rigorous enforcer of well-defined Rego semantics.
|
||||
|
||||
See main [Regorus page](https://github.com/microsoft/regorus) for more details about the project.
|
||||
|
||||
## Usage
|
||||
|
||||
Regorus Java is published to Maven Central with native libraries for the following:
|
||||
|
||||
- 64-bit Linux (kernel 3.2+, glibc 2.17+)
|
||||
- ARM64 Linux (kernel 4.1, glibc 2.17+)
|
||||
- 64-bit macOS (10.12+, Sierra+)
|
||||
- ARM64 macOS (11.0+, Big Sur+)
|
||||
- 64-bit MSVC (Windows 7+)
|
||||
|
||||
If you need to run it in a different OS or an architecture you need to manually [build it](#Building).
|
||||
|
||||
If you're on one of the supported platforms, you can just pull prebuilt JAR from Maven Central by declaring a dependency on `com.microsoft.regorus:regorus-java`.
|
||||
|
||||
With [Maven](https://maven.apache.org/):
|
||||
```xml
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.regorus</groupId>
|
||||
<artifactId>regorus-java</artifactId>
|
||||
<version>0.0.1</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
```
|
||||
|
||||
With [Gradle](https://gradle.org/):
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
implementation("com.microsoft.regorus:regorus-java:0.0.1")
|
||||
```
|
||||
|
||||
Afterwards you can use it as follows:
|
||||
|
||||
```java
|
||||
import com.microsoft.regorus.Engine;
|
||||
|
||||
public class Test {
|
||||
public static void main(String[] args) {
|
||||
try (Engine engine = new Engine()) {
|
||||
engine.addPolicy(
|
||||
"hello.rego",
|
||||
"package test\nmessage = concat(\", \", [input.message, data.message])"
|
||||
);
|
||||
engine.addDataJson("{\"message\":\"World!\"}");
|
||||
engine.setInputJson("{\"message\":\"Hello\"}");
|
||||
String resJson = engine.evalQuery("data.test.message");
|
||||
|
||||
System.out.println(resJson);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
And you can see the following output once you run it:
|
||||
```shell
|
||||
{"result":[{"expressions":[{"value":"Hello, World!","text":"data.test.message","location":{"row":1,"col":1}}]}]}
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
In order to build Regorus Java for a target platform, you need to install Rust target
|
||||
for that target platform first:
|
||||
|
||||
```bash
|
||||
$ rustup target add aarch64-apple-darwin
|
||||
```
|
||||
|
||||
Afterwards, you can build native library for that target using:
|
||||
```bash
|
||||
$ cargo build --release --target aarch64-apple-darwin
|
||||
```
|
||||
|
||||
You will then have a native library at `../../target/aarch64-apple-darwin/release/libregorus_java.dylib` depending on your target.
|
||||
|
||||
You can then build a JAR from source using:
|
||||
```bash
|
||||
$ mvn package
|
||||
```
|
||||
|
||||
And you will have a JAR at `./target/regorus-java-0.0.1.jar`.
|
||||
|
||||
You need to make sure both of the artifacts in Java's classpath.
|
||||
For example with `java` CLI:
|
||||
```bash
|
||||
$ java -Djava.library.path=../../target/aarch64-apple-darwin/release/ -cp target/regorus-java-0.0.1.jar Test.java
|
||||
```
|
||||
|
||||
93
bindings/java/com_microsoft_regorus_Engine.h
Normal file
93
bindings/java/com_microsoft_regorus_Engine.h
Normal file
@@ -0,0 +1,93 @@
|
||||
/* DO NOT EDIT THIS FILE - it is machine generated */
|
||||
#include <jni.h>
|
||||
/* Header for class com_microsoft_regorus_Engine */
|
||||
|
||||
#ifndef _Included_com_microsoft_regorus_Engine
|
||||
#define _Included_com_microsoft_regorus_Engine
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
/*
|
||||
* Class: com_microsoft_regorus_Engine
|
||||
* Method: nativeNewEngine
|
||||
* Signature: ()J
|
||||
*/
|
||||
JNIEXPORT jlong JNICALL Java_com_microsoft_regorus_Engine_nativeNewEngine
|
||||
(JNIEnv *, jclass);
|
||||
|
||||
/*
|
||||
* Class: com_microsoft_regorus_Engine
|
||||
* Method: nativeAddPolicy
|
||||
* Signature: (JLjava/lang/String;Ljava/lang/String;)V
|
||||
*/
|
||||
JNIEXPORT void JNICALL Java_com_microsoft_regorus_Engine_nativeAddPolicy
|
||||
(JNIEnv *, jclass, jlong, jstring, jstring);
|
||||
|
||||
/*
|
||||
* Class: com_microsoft_regorus_Engine
|
||||
* Method: nativeAddPolicyFromFile
|
||||
* Signature: (JLjava/lang/String;)V
|
||||
*/
|
||||
JNIEXPORT void JNICALL Java_com_microsoft_regorus_Engine_nativeAddPolicyFromFile
|
||||
(JNIEnv *, jclass, jlong, jstring);
|
||||
|
||||
/*
|
||||
* Class: com_microsoft_regorus_Engine
|
||||
* Method: nativeClearData
|
||||
* Signature: (J)V
|
||||
*/
|
||||
JNIEXPORT void JNICALL Java_com_microsoft_regorus_Engine_nativeClearData
|
||||
(JNIEnv *, jclass, jlong);
|
||||
|
||||
/*
|
||||
* Class: com_microsoft_regorus_Engine
|
||||
* Method: nativeAddDataJson
|
||||
* Signature: (JLjava/lang/String;)V
|
||||
*/
|
||||
JNIEXPORT void JNICALL Java_com_microsoft_regorus_Engine_nativeAddDataJson
|
||||
(JNIEnv *, jclass, jlong, jstring);
|
||||
|
||||
/*
|
||||
* Class: com_microsoft_regorus_Engine
|
||||
* Method: nativeAddDataJsonFromFile
|
||||
* Signature: (JLjava/lang/String;)V
|
||||
*/
|
||||
JNIEXPORT void JNICALL Java_com_microsoft_regorus_Engine_nativeAddDataJsonFromFile
|
||||
(JNIEnv *, jclass, jlong, jstring);
|
||||
|
||||
/*
|
||||
* Class: com_microsoft_regorus_Engine
|
||||
* Method: nativeSetInputJson
|
||||
* Signature: (JLjava/lang/String;)V
|
||||
*/
|
||||
JNIEXPORT void JNICALL Java_com_microsoft_regorus_Engine_nativeSetInputJson
|
||||
(JNIEnv *, jclass, jlong, jstring);
|
||||
|
||||
/*
|
||||
* Class: com_microsoft_regorus_Engine
|
||||
* Method: nativeSetInputJsonFromFile
|
||||
* Signature: (JLjava/lang/String;)V
|
||||
*/
|
||||
JNIEXPORT void JNICALL Java_com_microsoft_regorus_Engine_nativeSetInputJsonFromFile
|
||||
(JNIEnv *, jclass, jlong, jstring);
|
||||
|
||||
/*
|
||||
* Class: com_microsoft_regorus_Engine
|
||||
* Method: nativeEvalQuery
|
||||
* Signature: (JLjava/lang/String;)Ljava/lang/String;
|
||||
*/
|
||||
JNIEXPORT jstring JNICALL Java_com_microsoft_regorus_Engine_nativeEvalQuery
|
||||
(JNIEnv *, jclass, jlong, jstring);
|
||||
|
||||
/*
|
||||
* Class: com_microsoft_regorus_Engine
|
||||
* Method: nativeDestroyEngine
|
||||
* Signature: (J)V
|
||||
*/
|
||||
JNIEXPORT void JNICALL Java_com_microsoft_regorus_Engine_nativeDestroyEngine
|
||||
(JNIEnv *, jclass, jlong);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
138
bindings/java/pom.xml
Normal file
138
bindings/java/pom.xml
Normal file
@@ -0,0 +1,138 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright (c) Microsoft Corporation.
|
||||
Licensed under the MIT License.
|
||||
-->
|
||||
|
||||
<project>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.microsoft.regorus</groupId>
|
||||
<artifactId>regorus-java</artifactId>
|
||||
<version>0.0.1</version>
|
||||
|
||||
<name>Regorus Java</name>
|
||||
<description>Java bindings for Regorus - a fast, lightweight Rego interpreter written in Rust</description>
|
||||
<url>https://github.com/microsoft/regorus/bindings/java</url>
|
||||
|
||||
<licenses>
|
||||
<license>
|
||||
<name>MIT License</name>
|
||||
<url>https://opensource.org/blog/license/mit</url>
|
||||
</license>
|
||||
</licenses>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.compiler.source>1.8</maven.compiler.source>
|
||||
<maven.compiler.target>1.8</maven.compiler.target>
|
||||
</properties>
|
||||
|
||||
<distributionManagement>
|
||||
<repository>
|
||||
<id>ossrh</id>
|
||||
<name>Central Repository OSSRH</name>
|
||||
<url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
|
||||
</repository>
|
||||
</distributionManagement>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>3.8.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>2.10.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<resources>
|
||||
<resource>
|
||||
<!--
|
||||
Include native/ folder in built JAR.
|
||||
During CI build we build native libraries for various platforms
|
||||
and put them into native/ folder.
|
||||
See `.github/publish-java.yml`.
|
||||
-->
|
||||
<directory>${project.basedir}/native</directory>
|
||||
</resource>
|
||||
</resources>
|
||||
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<version>3.1.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<!-- Build a debug release for tests -->
|
||||
<id>build-native-lib-for-test</id>
|
||||
<phase>test-compile</phase>
|
||||
<goals>
|
||||
<goal>exec</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<executable>python3</executable>
|
||||
<arguments>
|
||||
<argument>${project.basedir}/tools/testbuild.py</argument>
|
||||
</arguments>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.2.5</version>
|
||||
<configuration>
|
||||
<!-- Add debug build to Java path, so it's discoverable by JVM. This is only for tests. -->
|
||||
<argLine>-Djava.library.path=${project.basedir}/target/debug:${java.library.path}</argLine>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<!-- Build javadoc JAR, this is required by Maven Central. -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>3.6.3</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-javadoc</id>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<!-- Build sources JAR, this is required by Maven Central. -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<version>3.3.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-sources</id>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<reporting>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-project-info-reports-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</reporting>
|
||||
</project>
|
||||
163
bindings/java/src/lib.rs
Normal file
163
bindings/java/src/lib.rs
Normal file
@@ -0,0 +1,163 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
use anyhow::Result;
|
||||
use jni::objects::{JClass, JObject, JString};
|
||||
use jni::sys::{jlong, jstring};
|
||||
use jni::JNIEnv;
|
||||
|
||||
use regorus::{Engine, Value};
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeNewEngine(
|
||||
_env: JNIEnv,
|
||||
_class: JClass,
|
||||
) -> jlong {
|
||||
let engine = Engine::new();
|
||||
Box::into_raw(Box::new(engine)) as jlong
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeAddPolicy(
|
||||
env: JNIEnv,
|
||||
_class: JClass,
|
||||
engine_ptr: jlong,
|
||||
path: JString,
|
||||
rego: JString,
|
||||
) {
|
||||
let _ = throw_err(env, |env| {
|
||||
let engine = unsafe { &mut *(engine_ptr as *mut Engine) };
|
||||
let path: String = env.get_string(&path)?.into();
|
||||
let rego: String = env.get_string(®o)?.into();
|
||||
engine.add_policy(path, rego)?;
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeAddPolicyFromFile(
|
||||
env: JNIEnv,
|
||||
_class: JClass,
|
||||
engine_ptr: jlong,
|
||||
path: JString,
|
||||
) {
|
||||
let _ = throw_err(env, |env| {
|
||||
let engine = unsafe { &mut *(engine_ptr as *mut Engine) };
|
||||
let path: String = env.get_string(&path)?.into();
|
||||
engine.add_policy_from_file(path)?;
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeClearData(
|
||||
env: JNIEnv,
|
||||
_class: JClass,
|
||||
engine_ptr: jlong,
|
||||
) {
|
||||
let _ = throw_err(env, |_env| {
|
||||
let engine = unsafe { &mut *(engine_ptr as *mut Engine) };
|
||||
engine.clear_data();
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeAddDataJson(
|
||||
env: JNIEnv,
|
||||
_class: JClass,
|
||||
engine_ptr: jlong,
|
||||
data: JString,
|
||||
) {
|
||||
let _ = throw_err(env, |env| {
|
||||
let engine = unsafe { &mut *(engine_ptr as *mut Engine) };
|
||||
let data: String = env.get_string(&data)?.into();
|
||||
engine.add_data_json(&data)?;
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeAddDataJsonFromFile(
|
||||
env: JNIEnv,
|
||||
_class: JClass,
|
||||
engine_ptr: jlong,
|
||||
path: JString,
|
||||
) {
|
||||
let _ = throw_err(env, |env| {
|
||||
let engine = unsafe { &mut *(engine_ptr as *mut Engine) };
|
||||
let path: String = env.get_string(&path)?.into();
|
||||
engine.add_data(Value::from_json_file(path)?)?;
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeSetInputJson(
|
||||
env: JNIEnv,
|
||||
_class: JClass,
|
||||
engine_ptr: jlong,
|
||||
input: JString,
|
||||
) {
|
||||
let _ = throw_err(env, |env| {
|
||||
let engine = unsafe { &mut *(engine_ptr as *mut Engine) };
|
||||
let input: String = env.get_string(&input)?.into();
|
||||
engine.set_input_json(&input)?;
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeSetInputJsonFromFile(
|
||||
env: JNIEnv,
|
||||
_class: JClass,
|
||||
engine_ptr: jlong,
|
||||
path: JString,
|
||||
) {
|
||||
let _ = throw_err(env, |env| {
|
||||
let engine = unsafe { &mut *(engine_ptr as *mut Engine) };
|
||||
let path: String = env.get_string(&path)?.into();
|
||||
engine.set_input(Value::from_json_file(&path)?);
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeEvalQuery(
|
||||
env: JNIEnv,
|
||||
_class: JClass,
|
||||
engine_ptr: jlong,
|
||||
query: JString,
|
||||
) -> jstring {
|
||||
let res = throw_err(env, |env| {
|
||||
let engine = unsafe { &mut *(engine_ptr as *mut Engine) };
|
||||
let query: String = env.get_string(&query)?.into();
|
||||
let results = engine.eval_query(query, false)?;
|
||||
let output = env.new_string(serde_json::to_string(&results)?)?;
|
||||
Ok(output.into_raw())
|
||||
});
|
||||
|
||||
match res {
|
||||
Ok(val) => val,
|
||||
Err(_) => JObject::null().into_raw(),
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "system" fn Java_com_microsoft_regorus_Engine_nativeDestroyEngine(
|
||||
_env: JNIEnv,
|
||||
_class: JClass,
|
||||
engine_ptr: jlong,
|
||||
) {
|
||||
let _engine = Box::from_raw(engine_ptr as *mut Engine);
|
||||
}
|
||||
|
||||
fn throw_err<T>(mut env: JNIEnv, mut f: impl FnMut(&mut JNIEnv) -> Result<T>) -> Result<T> {
|
||||
match f(&mut env) {
|
||||
Ok(val) => Ok(val),
|
||||
Err(err) => {
|
||||
env.throw(err.to_string())?;
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
199
bindings/java/src/main/java/com/microsoft/regorus/Engine.java
Normal file
199
bindings/java/src/main/java/com/microsoft/regorus/Engine.java
Normal file
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
* Licensed under the MIT License.
|
||||
**/
|
||||
|
||||
package com.microsoft.regorus;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* Regorus Engine.
|
||||
*/
|
||||
public class Engine implements AutoCloseable {
|
||||
// Methods exposed from Rust side, you can run
|
||||
// `javac -h . src/main/java/com/microsoft/regorus/Engine.java` to update
|
||||
// expected native header at `bindings/java/com_microsoft_regorus_Engine.h`
|
||||
// if you update the native API.
|
||||
private static native long nativeNewEngine();
|
||||
private static native void nativeAddPolicy(long enginePtr, String path, String rego);
|
||||
private static native void nativeAddPolicyFromFile(long enginePtr, String path);
|
||||
private static native void nativeClearData(long enginePtr);
|
||||
private static native void nativeAddDataJson(long enginePtr, String data);
|
||||
private static native void nativeAddDataJsonFromFile(long enginePtr, String path);
|
||||
private static native void nativeSetInputJson(long enginePtr, String input);
|
||||
private static native void nativeSetInputJsonFromFile(long enginePtr, String path);
|
||||
private static native String nativeEvalQuery(long enginePtr, String query);
|
||||
private static native void nativeDestroyEngine(long enginePtr);
|
||||
|
||||
// Pointer to Engine allocated on Rust's heap, all native methods works on
|
||||
// engine expects this pointer. It is free'd in `close` method.
|
||||
private final long enginePtr;
|
||||
|
||||
/**
|
||||
* Creates a new Regorus Engine.
|
||||
*/
|
||||
public Engine() {
|
||||
enginePtr = nativeNewEngine();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an inline Rego policy.
|
||||
*
|
||||
* @param filename Filename of this Rego policy.
|
||||
* @param rego Rego policy.
|
||||
*/
|
||||
public void addPolicy(String filename, String rego) {
|
||||
nativeAddPolicy(enginePtr, filename, rego);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a Rego policy from given path.
|
||||
*
|
||||
* @param path Path of the Rego policy.
|
||||
*/
|
||||
public void addPolicyFromFile(String path) {
|
||||
nativeAddPolicyFromFile(enginePtr, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the data document.
|
||||
*/
|
||||
public void clearData() {
|
||||
nativeClearData(enginePtr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds inline data document from given JSON.
|
||||
* The specified data document is merged into existing data document.
|
||||
* It will throw an error if new data conflicts with the existing document.
|
||||
*
|
||||
* Example:
|
||||
* addDataJson("[]") - Throws as it's not an object.
|
||||
* addDataJson('{"a": 1}') - Fine
|
||||
* addDataJson('{"b": 2}') - Fine, now {"a": 1, "b": 2}
|
||||
* addDataJson('{"b": 3}') - Throws as `b` conflicts.
|
||||
*
|
||||
* @see clearData
|
||||
*
|
||||
* @throws RuntimeException If data conflicts with the existing document
|
||||
* or data is not an object.
|
||||
*
|
||||
* @param data Inline data document.
|
||||
*/
|
||||
public void addDataJson(String data) throws RuntimeException {
|
||||
nativeAddDataJson(enginePtr, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds data document from given JSON file.
|
||||
* The specified data document is merged into existing data document.
|
||||
* It will throw an error if new data conflicts with the existing document.
|
||||
*
|
||||
* @see addDataJson
|
||||
* @see clearData
|
||||
*
|
||||
* @throws RuntimeException If data conflicts with the existing document
|
||||
* or data is not an object.
|
||||
*
|
||||
* @param path Path to JSON data document.
|
||||
*/
|
||||
public void addDataJsonFromFile(String path) throws RuntimeException {
|
||||
nativeAddDataJsonFromFile(enginePtr, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets inline JSON input.
|
||||
*
|
||||
* @param input inline JSON input.
|
||||
*/
|
||||
public void setInputJson(String input) {
|
||||
nativeSetInputJson(enginePtr, input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets JSON input from given path.
|
||||
*
|
||||
* @param path Path to JSON input.
|
||||
*/
|
||||
public void setInputJsonFromFile(String path) {
|
||||
nativeSetInputJsonFromFile(enginePtr, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates given Rego query and returns a JSON string as a result.
|
||||
*
|
||||
* @param query The Rego query.
|
||||
*
|
||||
* @return Query results as a JSON string.
|
||||
*/
|
||||
public String evalQuery(String query) {
|
||||
return nativeEvalQuery(enginePtr, query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
nativeDestroyEngine(enginePtr);
|
||||
}
|
||||
|
||||
// Loading native library from JAR is adapted from:
|
||||
// https://github.com/apache/opendal/blob/93e5f65bbf30df2fed4bdd95bb0685c73c6418c2/bindings/java/src/main/java/org/apache/opendal/NativeLibrary.java
|
||||
// https://github.com/apache/opendal/blob/93e5f65bbf30df2fed4bdd95bb0685c73c6418c2/bindings/java/src/main/java/org/apache/opendal/Environment.java
|
||||
static {
|
||||
// Build a Rust target triple, like: 'aarch64-unknown-linux-gnu'.
|
||||
final StringBuilder targetTripleBuilder = new StringBuilder();
|
||||
|
||||
final String arch = System.getProperty("os.arch").toLowerCase();
|
||||
if (arch.equals("aarch64")) {
|
||||
targetTripleBuilder.append("aarch64");
|
||||
} else {
|
||||
targetTripleBuilder.append("x86_64");
|
||||
}
|
||||
targetTripleBuilder.append("-");
|
||||
|
||||
final String os = System.getProperty("os.name").toLowerCase();
|
||||
if (os.startsWith("windows")) {
|
||||
targetTripleBuilder.append("pc-windows-msvc");
|
||||
} else if (os.startsWith("mac")) {
|
||||
targetTripleBuilder.append("apple-darwin");
|
||||
} else {
|
||||
targetTripleBuilder.append("unknown-linux-gnu");
|
||||
}
|
||||
|
||||
loadNativeLibrary(targetTripleBuilder.toString());
|
||||
}
|
||||
|
||||
private static void loadNativeLibrary(String targetTriple) {
|
||||
try {
|
||||
// try dynamic library - the search path can be configured via "-Djava.library.path"
|
||||
System.loadLibrary("regorus_java");
|
||||
return;
|
||||
} catch (UnsatisfiedLinkError ignore) {
|
||||
// ignore - try from classpath
|
||||
}
|
||||
|
||||
// Native libraries will be bundles into JARs like:
|
||||
// `aarch64-apple-darwin/libregorus_java.dylib`
|
||||
final String libraryName = System.mapLibraryName("regorus_java");
|
||||
final String libraryPath = "/" + targetTriple + "/" + libraryName;
|
||||
|
||||
try (final InputStream is = Engine.class.getResourceAsStream(libraryPath)) {
|
||||
if (is == null) {
|
||||
throw new RuntimeException("Cannot find " + libraryPath + "\nSee https://github.com/microsoft/regorus/tree/main/bindings/java for help.");
|
||||
}
|
||||
final int dot = libraryPath.indexOf('.');
|
||||
final File tmpFile = File.createTempFile(libraryPath.substring(0, dot), libraryPath.substring(dot));
|
||||
tmpFile.deleteOnExit();
|
||||
Files.copy(is, tmpFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
System.load(tmpFile.getAbsolutePath());
|
||||
} catch (IOException exception) {
|
||||
throw new RuntimeException(exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
* Licensed under the MIT License.
|
||||
**/
|
||||
package com.microsoft.regorus;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.ArrayList;
|
||||
import junit.framework.TestCase;
|
||||
import junit.framework.Assert;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
public class EngineTest extends TestCase
|
||||
{
|
||||
public void test_engine()
|
||||
{
|
||||
String resJson;
|
||||
try (Engine engine = new Engine()) {
|
||||
engine.addPolicy(
|
||||
"hello.rego",
|
||||
"package test\nmessage = concat(\", \", [input.message, data.message])"
|
||||
);
|
||||
engine.addDataJson("{\"message\":\"World!\"}");
|
||||
engine.setInputJson("{\"message\":\"Hello\"}");
|
||||
resJson = engine.evalQuery("data.test.message");
|
||||
}
|
||||
|
||||
Gson gson = new Gson();
|
||||
Map res = gson.fromJson(resJson, Map.class);
|
||||
ArrayList results = (ArrayList) res.get("result");
|
||||
ArrayList expressions = (ArrayList) ((Map) results.get(0)).get("expressions");
|
||||
Map expression = (Map) expressions.get(0);
|
||||
Assert.assertEquals("Hello, World!", expression.get("value"));
|
||||
}
|
||||
}
|
||||
17
bindings/java/tools/testbuild.py
Normal file
17
bindings/java/tools/testbuild.py
Normal file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
#
|
||||
# Builds Regorus Java to use in Java tests. See `pom.xml`.
|
||||
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
if __name__ == "__main__":
|
||||
basedir = Path(__file__).parent.parent
|
||||
|
||||
output = basedir / "target"
|
||||
Path(output).mkdir(exist_ok=True, parents=True)
|
||||
cmd = ["cargo", "build", "--target-dir", str(output)]
|
||||
print("$ " + subprocess.list2cmdline(cmd))
|
||||
subprocess.run(cmd, cwd=basedir, check=True)
|
||||
BIN
docs/coverage.png
Normal file
BIN
docs/coverage.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 607 KiB |
@@ -10,12 +10,16 @@ fn rego_eval(
|
||||
query: String,
|
||||
enable_tracing: bool,
|
||||
non_strict: bool,
|
||||
#[cfg(feature = "coverage")] coverage: bool,
|
||||
) -> Result<()> {
|
||||
// Create engine.
|
||||
let mut engine = regorus::Engine::new();
|
||||
|
||||
engine.set_strict_builtin_errors(!non_strict);
|
||||
|
||||
#[cfg(feature = "coverage")]
|
||||
engine.set_enable_coverage(coverage);
|
||||
|
||||
// Load files from given bundles.
|
||||
for dir in bundles.iter() {
|
||||
let entries =
|
||||
@@ -68,8 +72,15 @@ fn rego_eval(
|
||||
|
||||
// Evaluate query.
|
||||
let results = engine.eval_query(query, enable_tracing)?;
|
||||
|
||||
println!("{}", serde_json::to_string_pretty(&results)?);
|
||||
|
||||
#[cfg(feature = "coverage")]
|
||||
if coverage {
|
||||
let report = engine.get_coverage_report()?;
|
||||
println!("{}", report.to_colored_string()?);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -137,9 +148,14 @@ enum RegorusCommand {
|
||||
#[arg(long, short)]
|
||||
trace: bool,
|
||||
|
||||
// Non strict execution
|
||||
/// Perform non-strict evaluation. (default behavior of OPA).
|
||||
#[arg(long, short)]
|
||||
non_strict: bool,
|
||||
|
||||
/// Display coverage information
|
||||
#[cfg(feature = "coverage")]
|
||||
#[arg(long, short)]
|
||||
coverage: bool,
|
||||
},
|
||||
|
||||
/// Tokenize a Rego policy.
|
||||
@@ -183,7 +199,18 @@ fn main() -> Result<()> {
|
||||
query,
|
||||
trace,
|
||||
non_strict,
|
||||
} => rego_eval(&bundles, &data, input, query, trace, non_strict),
|
||||
#[cfg(feature = "coverage")]
|
||||
coverage,
|
||||
} => rego_eval(
|
||||
&bundles,
|
||||
&data,
|
||||
input,
|
||||
query,
|
||||
trace,
|
||||
non_strict,
|
||||
#[cfg(feature = "coverage")]
|
||||
coverage,
|
||||
),
|
||||
RegorusCommand::Lex { file, verbose } => rego_lex(file, verbose),
|
||||
RegorusCommand::Parse { file } => rego_parse(file),
|
||||
}
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
set -e
|
||||
|
||||
if ! command -v grcov > /dev/null; then
|
||||
cargo install grcov
|
||||
fi
|
||||
|
||||
if ! command -v llvm-profdata > /dev/null; then
|
||||
rustup component add llvm-tools-preview
|
||||
fi
|
||||
|
||||
#export LLVM_PROFILE_FILE='target/cargo-test-%p-%m.profraw'
|
||||
#export CARGO_INCREMENTAL=1
|
||||
#export RUSTFLAGS='-Cinstrument-coverage'
|
||||
|
||||
echo "Building with instrumentation"
|
||||
cargo build --all-targets
|
||||
|
||||
if [ "$1" == "--no-run" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Remove existing coverage information.
|
||||
rm -f target/*.profraw
|
||||
rm -rf target/coverage
|
||||
mkdir -p target/coverage
|
||||
|
||||
echo "Running tests"
|
||||
cargo test
|
||||
|
||||
# Generate html
|
||||
grcov target/ --binary-path ./target/debug/deps -s src/ -t html \
|
||||
--branch --ignore-not-existing --ignore '../*' --ignore "/*" -o target/coverage/html
|
||||
|
||||
if [ "$1" == "--show" ]; then
|
||||
echo "Opening report in browser"
|
||||
xdg-open target/coverage/html/index.html 2>/dev/null
|
||||
echo "Done"
|
||||
fi
|
||||
|
||||
# Generate markdown
|
||||
grcov target/ --binary-path ./target/debug/deps -s src/ -t markdown \
|
||||
--branch --ignore-not-existing --ignore '../*' --ignore "/*" -o target/coverage/markdown
|
||||
|
||||
cat target/coverage/markdown
|
||||
|
||||
# Print small-form table of files without 100% coverage.
|
||||
echo "Files without 100% coverage"
|
||||
while read p; do
|
||||
file=$(echo "$p" | cut -f 2 -d '|')
|
||||
percent=$(echo "$p" | cut -f 3 -d '|')
|
||||
missing=$(echo "$p" | cut -f 5 -d '|')
|
||||
|
||||
if [ -z "$file" ]; then
|
||||
break
|
||||
fi
|
||||
|
||||
# Trim percentage using xargs.
|
||||
case $(echo "$percent" | xargs) in
|
||||
"100%"|"100.00%")
|
||||
continue
|
||||
esac
|
||||
|
||||
|
||||
echo "| $file | $percent |"
|
||||
done < target/coverage/markdown
|
||||
|
||||
#TODO: Maybe use coveralls format (json) and query data to lockdown code coverage.
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
git stash
|
||||
cargo doc --no-deps
|
||||
git checkout docs
|
||||
rm -rf docs
|
||||
cp -r target/x86_64-unknown-linux-musl/doc ./docs
|
||||
echo "<meta http-equiv=\"refresh\" content=\"0; url=regorus/index.html\">" > docs/index.html
|
||||
git add docs
|
||||
git commit -s
|
||||
git push
|
||||
git checkout -
|
||||
git stash pop
|
||||
@@ -6,7 +6,7 @@ set -eo pipefail
|
||||
|
||||
if [ -f Cargo.toml ]; then
|
||||
# Ensure that all targets can be built.
|
||||
cargo build --all-targets
|
||||
cargo build -r --all-targets
|
||||
|
||||
#Ensure that code is correctly formatted.
|
||||
cargo fmt --check || (echo "Run cargo fmt to fix formatting" && exit 1)
|
||||
|
||||
@@ -9,11 +9,12 @@ if [ -f Cargo.toml ]; then
|
||||
dir=$(dirname "${BASH_SOURCE[0]}")
|
||||
"$dir/pre-commit"
|
||||
|
||||
# Ensure that the public API works
|
||||
cargo test -r --doc
|
||||
|
||||
# Ensure that all tests pass
|
||||
# Also generate coverage information.
|
||||
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||
scripts/coverage
|
||||
fi
|
||||
cargo test -r
|
||||
cargo test -r --test aci
|
||||
|
||||
# Ensure that OPA conformance tests don't regress.
|
||||
cargo test -r --features opa-testutil --test opa -- $(tr '\n' ' ' < tests/opa.passing)
|
||||
|
||||
11
src/ast.rs
11
src/ast.rs
@@ -2,6 +2,7 @@
|
||||
// Licensed under the MIT License.
|
||||
|
||||
use crate::lexer::*;
|
||||
use crate::Rc;
|
||||
|
||||
use std::ops::Deref;
|
||||
|
||||
@@ -37,7 +38,7 @@ pub enum AssignOp {
|
||||
}
|
||||
|
||||
pub struct NodeRef<T> {
|
||||
r: std::rc::Rc<T>,
|
||||
r: Rc<T>,
|
||||
}
|
||||
|
||||
impl<T> Clone for NodeRef<T> {
|
||||
@@ -54,7 +55,7 @@ impl<T: std::fmt::Debug> std::fmt::Debug for NodeRef<T> {
|
||||
|
||||
impl<T> std::cmp::PartialEq for NodeRef<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
std::rc::Rc::as_ptr(&self.r).eq(&std::rc::Rc::as_ptr(&other.r))
|
||||
Rc::as_ptr(&self.r).eq(&Rc::as_ptr(&other.r))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +63,7 @@ impl<T> std::cmp::Eq for NodeRef<T> {}
|
||||
|
||||
impl<T> std::cmp::Ord for NodeRef<T> {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
std::rc::Rc::as_ptr(&self.r).cmp(&std::rc::Rc::as_ptr(&other.r))
|
||||
Rc::as_ptr(&self.r).cmp(&Rc::as_ptr(&other.r))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,9 +89,7 @@ impl<T> AsRef<T> for NodeRef<T> {
|
||||
|
||||
impl<T> NodeRef<T> {
|
||||
pub fn new(t: T) -> Self {
|
||||
Self {
|
||||
r: std::rc::Rc::new(t),
|
||||
}
|
||||
Self { r: Rc::new(t) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,12 @@ use crate::ast::{Expr, Ref};
|
||||
use crate::builtins;
|
||||
use crate::builtins::utils::{ensure_args_count, ensure_array, ensure_numeric};
|
||||
use crate::lexer::Span;
|
||||
use crate::value::Value;
|
||||
use crate::Rc;
|
||||
use crate::Value;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Result;
|
||||
use std::rc::Rc;
|
||||
|
||||
pub fn register(m: &mut HashMap<&'static str, builtins::BuiltinFcn>) {
|
||||
m.insert("array.concat", (concat, 2));
|
||||
|
||||
@@ -17,10 +17,12 @@ pub fn register(m: &mut HashMap<&'static str, builtins::BuiltinFcn>) {
|
||||
m.insert("print", (print, MAX_ARGS));
|
||||
}
|
||||
|
||||
// Symbol analyzer must ensure that vars used by print are defined before
|
||||
// the print statement. Scheduler must ensure the above constraint.
|
||||
// Additionally interpreter must allow undefined inputs to print.
|
||||
fn print(span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
|
||||
pub fn print_to_string(
|
||||
span: &Span,
|
||||
_params: &[Ref<Expr>],
|
||||
args: &[Value],
|
||||
_strict: bool,
|
||||
) -> Result<String> {
|
||||
if args.len() > MAX_ARGS as usize {
|
||||
bail!(span.error("print supports up to 100 arguments"));
|
||||
}
|
||||
@@ -34,8 +36,17 @@ fn print(span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> R
|
||||
};
|
||||
}
|
||||
|
||||
Ok(msg)
|
||||
}
|
||||
|
||||
// Symbol analyzer must ensure that vars used by print are defined before
|
||||
// the print statement. Scheduler must ensure the above constraint.
|
||||
// Additionally interpreter must allow undefined inputs to print.
|
||||
fn print(span: &Span, params: &[Ref<Expr>], args: &[Value], strict: bool) -> Result<Value> {
|
||||
let msg = print_to_string(span, params, args, strict)?;
|
||||
|
||||
if !msg.is_empty() {
|
||||
println!("{}", &msg[1..]);
|
||||
eprintln!("{}", &msg[1..]);
|
||||
}
|
||||
Ok(Value::Bool(true))
|
||||
}
|
||||
|
||||
@@ -3,19 +3,64 @@
|
||||
|
||||
use crate::ast::{Expr, Ref};
|
||||
use crate::builtins;
|
||||
use crate::builtins::utils::ensure_args_count;
|
||||
use crate::builtins::utils::{ensure_args_count, ensure_string};
|
||||
|
||||
use crate::lexer::Span;
|
||||
use crate::value::Value;
|
||||
|
||||
use itertools::Itertools;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
pub fn register(m: &mut HashMap<&'static str, builtins::BuiltinFcn>) {
|
||||
m.insert("io.jwt.decode", (jwt_decode, 1));
|
||||
m.insert("io.jwt.decode_verify", (jwt_decode_verify, 2));
|
||||
}
|
||||
|
||||
fn decode(span: &Span, jwt: String, strict: bool) -> Result<Value> {
|
||||
let Some((Ok(header), Ok(payload), Ok(signature))) = jwt
|
||||
.split('.')
|
||||
.map(|p| data_encoding::BASE64URL_NOPAD.decode(p.as_bytes()))
|
||||
.collect_tuple()
|
||||
else {
|
||||
if strict {
|
||||
bail!(span.error("invalid jwt token"));
|
||||
}
|
||||
return Ok(Value::Undefined);
|
||||
};
|
||||
|
||||
let header = String::from_utf8_lossy(&header).to_string();
|
||||
let payload = String::from_utf8_lossy(&payload).to_string();
|
||||
let signature = data_encoding::HEXLOWER_PERMISSIVE.encode(&signature);
|
||||
|
||||
let signature = Value::String(signature.into());
|
||||
let header = Value::from_json_str(&header)?;
|
||||
|
||||
if header["enc"] != Value::Undefined {
|
||||
bail!(span.error("JWT is a JWE object, which is not supported"));
|
||||
}
|
||||
|
||||
if header["cty"] == "JWT".into() {
|
||||
if payload.len() <= 2 || !payload.starts_with('"') || !payload.ends_with('"') {
|
||||
bail!(span.error("invalid nested JWT"));
|
||||
}
|
||||
// Ignore ""
|
||||
decode(span, payload[1..payload.len() - 1].to_string(), strict)
|
||||
} else {
|
||||
let payload = Value::from_json_str(&payload)?;
|
||||
Ok(Value::from_array([header, payload, signature].into()))
|
||||
}
|
||||
}
|
||||
|
||||
fn jwt_decode(span: &Span, params: &[Ref<Expr>], args: &[Value], strict: bool) -> Result<Value> {
|
||||
let name = "io.jwt.decode";
|
||||
ensure_args_count(span, name, params, args, 1)?;
|
||||
let jwt = ensure_string(name, ¶ms[0], &args[0])?;
|
||||
|
||||
decode(span, jwt.to_string(), strict) //header, payload, signature, strict)
|
||||
}
|
||||
|
||||
fn jwt_decode_verify(
|
||||
span: &Span,
|
||||
params: &[Ref<Expr>],
|
||||
|
||||
@@ -54,6 +54,8 @@ use lazy_static::lazy_static;
|
||||
|
||||
pub type BuiltinFcn = (fn(&Span, &[Ref<Expr>], &[Value], bool) -> Result<Value>, u8);
|
||||
|
||||
pub use debugging::print_to_string;
|
||||
|
||||
#[cfg(feature = "deprecated")]
|
||||
pub use deprecated::DEPRECATED;
|
||||
|
||||
|
||||
@@ -5,11 +5,11 @@ use crate::ast::{Expr, Ref};
|
||||
use crate::builtins;
|
||||
use crate::builtins::utils::{ensure_args_count, ensure_array, ensure_object};
|
||||
use crate::lexer::Span;
|
||||
use crate::value::Value;
|
||||
use crate::Rc;
|
||||
use crate::Value;
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
use std::iter::Iterator;
|
||||
use std::rc::Rc;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
|
||||
@@ -157,7 +157,9 @@ fn to_string(v: &Value, unescape: bool) -> String {
|
||||
match v {
|
||||
Value::Null => "null".to_owned(),
|
||||
Value::Bool(b) => b.to_string(),
|
||||
Value::String(s) if unescape => serde_json::to_string(&s).unwrap_or(s.as_ref().to_string()),
|
||||
Value::String(s) if unescape => {
|
||||
serde_json::to_string(s.as_ref()).unwrap_or(s.as_ref().to_string())
|
||||
}
|
||||
Value::String(s) => s.as_ref().to_string(),
|
||||
Value::Number(n) => n.format_decimal(),
|
||||
Value::Array(a) => {
|
||||
|
||||
@@ -599,66 +599,82 @@ mod tests {
|
||||
for (input, expected_dur) in [
|
||||
// simple
|
||||
("0", Duration::zero()),
|
||||
("5s", Duration::seconds(5)),
|
||||
("30s", Duration::seconds(30)),
|
||||
("1478s", Duration::seconds(1478)),
|
||||
("5s", Duration::try_seconds(5).unwrap()),
|
||||
("30s", Duration::try_seconds(30).unwrap()),
|
||||
("1478s", Duration::try_seconds(1478).unwrap()),
|
||||
// sign
|
||||
("-5s", -Duration::seconds(5)),
|
||||
("+5s", Duration::seconds(5)),
|
||||
("-5s", -Duration::try_seconds(5).unwrap()),
|
||||
("+5s", Duration::try_seconds(5).unwrap()),
|
||||
("-0", Duration::zero()),
|
||||
("+0", Duration::zero()),
|
||||
// decimal
|
||||
("5.0s", Duration::seconds(5)),
|
||||
("5.6s", Duration::seconds(5) + Duration::milliseconds(600)),
|
||||
("5.s", Duration::seconds(5)),
|
||||
(".5s", Duration::milliseconds(500)),
|
||||
("1.0s", Duration::seconds(1)),
|
||||
("1.00s", Duration::seconds(1)),
|
||||
("1.004s", Duration::seconds(1) + Duration::milliseconds(4)),
|
||||
("1.0040s", Duration::seconds(1) + Duration::milliseconds(4)),
|
||||
("5.0s", Duration::try_seconds(5).unwrap()),
|
||||
(
|
||||
"5.6s",
|
||||
Duration::try_seconds(5).unwrap() + Duration::try_milliseconds(600).unwrap(),
|
||||
),
|
||||
("5.s", Duration::try_seconds(5).unwrap()),
|
||||
(".5s", Duration::try_milliseconds(500).unwrap()),
|
||||
("1.0s", Duration::try_seconds(1).unwrap()),
|
||||
("1.00s", Duration::try_seconds(1).unwrap()),
|
||||
(
|
||||
"1.004s",
|
||||
Duration::try_seconds(1).unwrap() + Duration::try_milliseconds(4).unwrap(),
|
||||
),
|
||||
(
|
||||
"1.0040s",
|
||||
Duration::try_seconds(1).unwrap() + Duration::try_milliseconds(4).unwrap(),
|
||||
),
|
||||
(
|
||||
"100.00100s",
|
||||
Duration::seconds(100) + Duration::milliseconds(1),
|
||||
Duration::try_seconds(100).unwrap() + Duration::try_milliseconds(1).unwrap(),
|
||||
),
|
||||
// different units
|
||||
("10ns", Duration::nanoseconds(10)),
|
||||
("11us", Duration::microseconds(11)),
|
||||
("12µs", Duration::microseconds(12)), // U+00B5
|
||||
("12μs", Duration::microseconds(12)), // U+03BC
|
||||
("13ms", Duration::milliseconds(13)),
|
||||
("14s", Duration::seconds(14)),
|
||||
("15m", Duration::minutes(15)),
|
||||
("16h", Duration::hours(16)),
|
||||
("13ms", Duration::try_milliseconds(13).unwrap()),
|
||||
("14s", Duration::try_seconds(14).unwrap()),
|
||||
("15m", Duration::try_minutes(15).unwrap()),
|
||||
("16h", Duration::try_hours(16).unwrap()),
|
||||
// composite durations
|
||||
("3h30m", Duration::hours(3) + Duration::minutes(30)),
|
||||
(
|
||||
"3h30m",
|
||||
Duration::try_hours(3).unwrap() + Duration::try_minutes(30).unwrap(),
|
||||
),
|
||||
(
|
||||
"10.5s4m",
|
||||
Duration::minutes(4) + Duration::seconds(10) + Duration::milliseconds(500),
|
||||
Duration::try_minutes(4).unwrap()
|
||||
+ Duration::try_seconds(10).unwrap()
|
||||
+ Duration::try_milliseconds(500).unwrap(),
|
||||
),
|
||||
(
|
||||
"-2m3.4s",
|
||||
-(Duration::minutes(2) + Duration::seconds(3) + Duration::milliseconds(400)),
|
||||
-(Duration::try_minutes(2).unwrap()
|
||||
+ Duration::try_seconds(3).unwrap()
|
||||
+ Duration::try_milliseconds(400).unwrap()),
|
||||
),
|
||||
(
|
||||
"1h2m3s4ms5us6ns",
|
||||
Duration::hours(1)
|
||||
+ Duration::minutes(2)
|
||||
+ Duration::seconds(3)
|
||||
+ Duration::milliseconds(4)
|
||||
Duration::try_hours(1).unwrap()
|
||||
+ Duration::try_minutes(2).unwrap()
|
||||
+ Duration::try_seconds(3).unwrap()
|
||||
+ Duration::try_milliseconds(4).unwrap()
|
||||
+ Duration::microseconds(5)
|
||||
+ Duration::nanoseconds(6),
|
||||
),
|
||||
(
|
||||
"39h9m14.425s",
|
||||
Duration::hours(39)
|
||||
+ Duration::minutes(9)
|
||||
+ Duration::seconds(14)
|
||||
+ Duration::milliseconds(425),
|
||||
Duration::try_hours(39).unwrap()
|
||||
+ Duration::try_minutes(9).unwrap()
|
||||
+ Duration::try_seconds(14).unwrap()
|
||||
+ Duration::try_milliseconds(425).unwrap(),
|
||||
),
|
||||
// large value
|
||||
("52763797000ns", Duration::nanoseconds(52763797000)),
|
||||
// more than 9 digits after decimal point, see https://golang.org/issue/6617
|
||||
("0.3333333333333333333h", Duration::minutes(20)),
|
||||
("0.3333333333333333333h", Duration::try_minutes(20).unwrap()),
|
||||
// 9007199254740993 = 1<<53+1 cannot be stored precisely in a float64
|
||||
("9007199254740993ns", Duration::nanoseconds((1 << 53) + 1)),
|
||||
// largest duration that can be represented by int64 in nanoseconds
|
||||
@@ -679,11 +695,16 @@ mod tests {
|
||||
// largest negative round trip value, see https://golang.org/issue/48629
|
||||
("-2562047h47m16.854775808s", Duration::nanoseconds(i64::MIN)),
|
||||
// huge string; issue 15011.
|
||||
("0.100000000000000000000h", Duration::minutes(6)),
|
||||
(
|
||||
"0.100000000000000000000h",
|
||||
Duration::try_minutes(6).unwrap(),
|
||||
),
|
||||
// This value tests the first overflow check in leadingFraction.
|
||||
(
|
||||
"0.830103483285477580700h",
|
||||
Duration::minutes(49) + Duration::seconds(48) + Duration::nanoseconds(372539827),
|
||||
Duration::try_minutes(49).unwrap()
|
||||
+ Duration::try_seconds(48).unwrap()
|
||||
+ Duration::nanoseconds(372539827),
|
||||
),
|
||||
] {
|
||||
let dur = parse_duration(input).unwrap();
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
use crate::ast::{Expr, Ref};
|
||||
use crate::lexer::Span;
|
||||
use crate::number::Number;
|
||||
use crate::value::Value;
|
||||
use crate::Rc;
|
||||
use crate::Value;
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::rc::Rc;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
|
||||
237
src/engine.rs
237
src/engine.rs
@@ -17,7 +17,7 @@ use anyhow::{bail, Result};
|
||||
|
||||
/// The Rego evaluation engine.
|
||||
///
|
||||
#[derive(Clone)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Engine {
|
||||
modules: Vec<Ref<Module>>,
|
||||
interpreter: Interpreter,
|
||||
@@ -120,6 +120,11 @@ impl Engine {
|
||||
self.interpreter.set_input(input);
|
||||
}
|
||||
|
||||
pub fn set_input_json(&mut self, input_json: &str) -> Result<()> {
|
||||
self.set_input(Value::from_json_str(input_json)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clear the data document.
|
||||
///
|
||||
/// The data document will be reset to an empty object.
|
||||
@@ -182,6 +187,10 @@ impl Engine {
|
||||
self.interpreter.get_data_mut().merge(data)
|
||||
}
|
||||
|
||||
pub fn add_data_json(&mut self, data_json: &str) -> Result<()> {
|
||||
self.add_data(Value::from_json_str(data_json)?)
|
||||
}
|
||||
|
||||
/// Set whether builtins should raise errors strictly or not.
|
||||
///
|
||||
/// Regorus differs from OPA in that by default builtins will
|
||||
@@ -224,7 +233,7 @@ impl Engine {
|
||||
/// // Load input and make query.
|
||||
/// engine.set_input(Value::new_object());
|
||||
/// let results = engine.eval_query("data.framework.mount_overlay.allowed".to_string(), false)?;
|
||||
/// assert!(results.result.is_empty());
|
||||
/// assert_eq!(results.result[0].expressions[0].value, Value::from(false));
|
||||
///
|
||||
/// // Evaluate query with different inputs.
|
||||
/// engine.set_input(Value::from_json_file("tests/aci/input.json")?);
|
||||
@@ -232,7 +241,127 @@ impl Engine {
|
||||
/// assert_eq!(results.result[0].expressions[0].value, Value::from(true));
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn eval_query(&mut self, query: String, enable_tracing: bool) -> Result<QueryResults> {
|
||||
self.prepare_for_eval(enable_tracing)?;
|
||||
self.interpreter.clean_internal_evaluation_state();
|
||||
|
||||
self.interpreter.create_rule_prefixes()?;
|
||||
let query_module = {
|
||||
let source = Source::new(
|
||||
"<query_module.rego>".to_owned(),
|
||||
"package __internal_query_module".to_owned(),
|
||||
);
|
||||
Ref::new(Parser::new(&source)?.parse()?)
|
||||
};
|
||||
|
||||
// Parse the query.
|
||||
let query_source = Source::new("<query.rego>".to_string(), query);
|
||||
let mut parser = Parser::new(&query_source)?;
|
||||
let query_node = parser.parse_user_query()?;
|
||||
if query_node.span.text() == "data" {
|
||||
self.eval_modules(enable_tracing)?;
|
||||
}
|
||||
let query_schedule = Analyzer::new().analyze_query_snippet(&self.modules, &query_node)?;
|
||||
self.interpreter.eval_user_query(
|
||||
&query_module,
|
||||
&query_node,
|
||||
&query_schedule,
|
||||
enable_tracing,
|
||||
)
|
||||
}
|
||||
|
||||
/// Evaluate a Rego query that produces a boolean value.
|
||||
///
|
||||
///
|
||||
/// This function should be preferred over [`Engine::eval_query`] if just a `true`/`false`
|
||||
/// value is desired instead of [`QueryResults`].
|
||||
///
|
||||
/// ```
|
||||
/// # use regorus::*;
|
||||
/// # fn main() -> anyhow::Result<()> {
|
||||
/// # let mut engine = Engine::new();
|
||||
///
|
||||
/// let enable_tracing = false;
|
||||
/// assert_eq!(engine.eval_bool_query("1 > 2".to_string(), enable_tracing)?, false);
|
||||
/// assert_eq!(engine.eval_bool_query("1 < 2".to_string(), enable_tracing)?, true);
|
||||
///
|
||||
/// // Non boolean queries will raise an error.
|
||||
/// assert!(engine.eval_bool_query("1+1".to_string(), enable_tracing).is_err());
|
||||
///
|
||||
/// // Queries producing multiple values will raise an error.
|
||||
/// assert!(engine.eval_bool_query("true; true".to_string(), enable_tracing).is_err());
|
||||
///
|
||||
/// // Queries producing no values will raise an error.
|
||||
/// assert!(engine.eval_bool_query("true; false; true".to_string(), enable_tracing).is_err());
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn eval_bool_query(&mut self, query: String, enable_tracing: bool) -> Result<bool> {
|
||||
let results = self.eval_query(query, enable_tracing)?;
|
||||
match results.result.len() {
|
||||
0 => bail!("query did not produce any values"),
|
||||
1 if results.result[0].expressions.len() == 1 => {
|
||||
results.result[0].expressions[0].value.as_bool().copied()
|
||||
}
|
||||
_ => bail!("query produced more than one value"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluate an `allow` query.
|
||||
///
|
||||
/// This is a wrapper over [`Engine::eval_bool_query`] that returns true only if the
|
||||
/// boolean query succeed and produced a `true` value.
|
||||
///
|
||||
/// ```
|
||||
/// # use regorus::*;
|
||||
/// # fn main() -> anyhow::Result<()> {
|
||||
/// # let mut engine = Engine::new();
|
||||
///
|
||||
/// let enable_tracing = false;
|
||||
/// assert_eq!(engine.eval_allow_query("1 > 2".to_string(), enable_tracing), false);
|
||||
/// assert_eq!(engine.eval_allow_query("1 < 2".to_string(), enable_tracing), true);
|
||||
///
|
||||
/// assert_eq!(engine.eval_allow_query("1+1".to_string(), enable_tracing), false);
|
||||
/// assert_eq!(engine.eval_allow_query("true; true".to_string(), enable_tracing), false);
|
||||
/// assert_eq!(engine.eval_allow_query("true; false; true".to_string(), enable_tracing), false);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
pub fn eval_allow_query(&mut self, query: String, enable_tracing: bool) -> bool {
|
||||
matches!(self.eval_bool_query(query, enable_tracing), Ok(true))
|
||||
}
|
||||
|
||||
/// Evaluate a `deny` query.
|
||||
///
|
||||
/// This is a wrapper over [`Engine::eval_bool_query`] that returns false only if the
|
||||
/// boolean query succeed and produced a `false` value.
|
||||
/// ```
|
||||
/// # use regorus::*;
|
||||
/// # fn main() -> anyhow::Result<()> {
|
||||
/// # let mut engine = Engine::new();
|
||||
///
|
||||
/// let enable_tracing = false;
|
||||
/// assert_eq!(engine.eval_deny_query("1 > 2".to_string(), enable_tracing), false);
|
||||
/// assert_eq!(engine.eval_deny_query("1 < 2".to_string(), enable_tracing), true);
|
||||
///
|
||||
/// assert_eq!(engine.eval_deny_query("1+1".to_string(), enable_tracing), true);
|
||||
/// assert_eq!(engine.eval_deny_query("true; true".to_string(), enable_tracing), true);
|
||||
/// assert_eq!(engine.eval_deny_query("true; false; true".to_string(), enable_tracing), true);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
pub fn eval_deny_query(&mut self, query: String, enable_tracing: bool) -> bool {
|
||||
!matches!(self.eval_bool_query(query, enable_tracing), Ok(false))
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
/// Evaluate the given query and all the rules in the supplied policies.
|
||||
///
|
||||
/// This is mainly used for testing Regorus itself.
|
||||
pub fn eval_query_and_all_rules(
|
||||
&mut self,
|
||||
query: String,
|
||||
enable_tracing: bool,
|
||||
) -> Result<QueryResults> {
|
||||
self.eval_modules(enable_tracing)?;
|
||||
|
||||
let query_module = {
|
||||
@@ -423,8 +552,8 @@ impl Engine {
|
||||
/// "#.to_string()
|
||||
/// )?;
|
||||
///
|
||||
/// // Evaluation fails since y is not defined.
|
||||
/// assert!(engine.eval_query("data.invalid.y".to_string(), false).is_err());
|
||||
/// // Evaluation fails since rule x calls an extension with out parameter.
|
||||
/// assert!(engine.eval_query("data.invalid.x".to_string(), false).is_err());
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
@@ -436,4 +565,104 @@ impl Engine {
|
||||
) -> Result<()> {
|
||||
self.interpreter.add_extension(path, nargs, extension)
|
||||
}
|
||||
|
||||
#[cfg(feature = "coverage")]
|
||||
#[cfg_attr(doc_cfg, doc(cfg(feature = "coverage")))]
|
||||
/// Get the coverage report.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use regorus::*;
|
||||
/// # use anyhow::{bail, Result};
|
||||
/// # fn main() -> Result<()> {
|
||||
/// let mut engine = Engine::new();
|
||||
///
|
||||
/// engine.add_policy(
|
||||
/// "policy.rego".to_string(),
|
||||
/// r#"
|
||||
/// package test # Line 2
|
||||
///
|
||||
/// x = y { # Line 4
|
||||
/// input.a > 2 # Line 5
|
||||
/// y = 5 # Line 6
|
||||
/// }
|
||||
/// "#.to_string()
|
||||
/// )?;
|
||||
///
|
||||
/// // Enable coverage.
|
||||
/// engine.set_enable_coverage(true);
|
||||
///
|
||||
/// engine.eval_query("data".to_string(), false)?;
|
||||
///
|
||||
/// let report = engine.get_coverage_report()?;
|
||||
/// assert_eq!(report.files[0].path, "policy.rego");
|
||||
///
|
||||
/// // Only line 5 is evaluated.
|
||||
/// assert_eq!(report.files[0].covered.iter().cloned().collect::<Vec<u32>>(), vec![5]);
|
||||
///
|
||||
/// // Line 4 and 6 are not evaluated.
|
||||
/// assert_eq!(report.files[0].not_covered.iter().cloned().collect::<Vec<u32>>(), vec![4, 6]);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// See also [`crate::coverage::Report::to_colored_string`].
|
||||
pub fn get_coverage_report(&self) -> Result<crate::coverage::Report> {
|
||||
self.interpreter.get_coverage_report()
|
||||
}
|
||||
|
||||
#[cfg(feature = "coverage")]
|
||||
#[cfg_attr(doc_cfg, doc(cfg(feature = "coverage")))]
|
||||
/// Enable/disable policy coverage.
|
||||
///
|
||||
/// If `enable` is different from the current value, then any existing coverage
|
||||
/// information will be cleared.
|
||||
pub fn set_enable_coverage(&mut self, enable: bool) {
|
||||
self.interpreter.set_enable_coverage(enable)
|
||||
}
|
||||
|
||||
#[cfg(feature = "coverage")]
|
||||
#[cfg_attr(doc_cfg, doc(cfg(feature = "coverage")))]
|
||||
/// Clear the gathered policy coverage data.
|
||||
pub fn clear_coverage_data(&mut self) {
|
||||
self.interpreter.clear_coverage_data()
|
||||
}
|
||||
|
||||
/// Gather output from print statements instead of emiting to stderr.
|
||||
///
|
||||
/// See [`Engine::take_prints`].
|
||||
pub fn set_gather_prints(&mut self, b: bool) {
|
||||
self.interpreter.set_gather_prints(b);
|
||||
}
|
||||
|
||||
/// Take the gathered output of print statements.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use regorus::*;
|
||||
/// # use anyhow::{bail, Result};
|
||||
/// # fn main() -> Result<()> {
|
||||
/// let mut engine = Engine::new();
|
||||
///
|
||||
/// // Print to stderr.
|
||||
/// engine.eval_query("print(\"Hello\")".to_string(), false)?;
|
||||
///
|
||||
/// // Configure gathering print statements.
|
||||
/// engine.set_gather_prints(true);
|
||||
///
|
||||
/// // Execute query.
|
||||
/// engine.eval_query("print(\"Hello\")".to_string(), false)?;
|
||||
///
|
||||
/// // Take and clear prints.
|
||||
/// let prints = engine.take_prints()?;
|
||||
/// assert_eq!(prints.len(), 1);
|
||||
/// assert!(prints[0].contains("Hello"));
|
||||
///
|
||||
/// for p in prints {
|
||||
/// println!("{p}");
|
||||
/// }
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn take_prints(&mut self) -> Result<Vec<String>> {
|
||||
self.interpreter.take_prints()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use crate::parser::Parser;
|
||||
use crate::scheduler::*;
|
||||
use crate::utils::*;
|
||||
use crate::value::*;
|
||||
use crate::Rc;
|
||||
use crate::{Expression, Extension, Location, QueryResult, QueryResults};
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
@@ -16,7 +17,6 @@ use log::info;
|
||||
use std::collections::btree_map::Entry as BTreeMapEntry;
|
||||
use std::collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap};
|
||||
use std::ops::Bound::*;
|
||||
use std::rc::Rc;
|
||||
use std::str::FromStr;
|
||||
|
||||
type Scope = BTreeMap<SourceStr, Value>;
|
||||
@@ -28,6 +28,7 @@ type State = (
|
||||
Value,
|
||||
Value,
|
||||
BTreeSet<Ref<Rule>>,
|
||||
Value,
|
||||
BTreeMap<String, FunctionModifier>,
|
||||
BTreeMap<Vec<Value>, (Value, Ref<Expr>)>,
|
||||
);
|
||||
@@ -38,7 +39,7 @@ enum FunctionModifier {
|
||||
Value(Value),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Interpreter {
|
||||
modules: Vec<Ref<Module>>,
|
||||
module: Option<Ref<Module>>,
|
||||
@@ -57,15 +58,24 @@ pub struct Interpreter {
|
||||
rules: HashMap<String, Vec<Ref<Rule>>>,
|
||||
default_rules: HashMap<String, Vec<DefaultRuleInfo>>,
|
||||
processed: BTreeSet<Ref<Rule>>,
|
||||
processed_paths: Value,
|
||||
rule_values: BTreeMap<Vec<Value>, (Value, Ref<Expr>)>,
|
||||
active_rules: Vec<Ref<Rule>>,
|
||||
builtins_cache: BTreeMap<(&'static str, Vec<Value>), Value>,
|
||||
no_rules_lookup: bool,
|
||||
traces: Option<Vec<std::rc::Rc<str>>>,
|
||||
traces: Option<Vec<Rc<str>>>,
|
||||
allow_deprecated: bool,
|
||||
strict_builtin_errors: bool,
|
||||
imports: BTreeMap<String, Ref<Expr>>,
|
||||
extensions: HashMap<String, (u8, Box<dyn Extension>)>,
|
||||
extensions: HashMap<String, (u8, Rc<Box<dyn Extension>>)>,
|
||||
|
||||
#[cfg(feature = "coverage")]
|
||||
coverage: HashMap<Source, Vec<bool>>,
|
||||
#[cfg(feature = "coverage")]
|
||||
enable_coverage: bool,
|
||||
|
||||
gather_prints: bool,
|
||||
prints: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for Interpreter {
|
||||
@@ -168,6 +178,7 @@ impl Interpreter {
|
||||
rules: HashMap::new(),
|
||||
default_rules: HashMap::new(),
|
||||
processed: BTreeSet::new(),
|
||||
processed_paths: Value::new_object(),
|
||||
rule_values: BTreeMap::new(),
|
||||
active_rules: vec![],
|
||||
builtins_cache: BTreeMap::new(),
|
||||
@@ -177,6 +188,14 @@ impl Interpreter {
|
||||
strict_builtin_errors: true,
|
||||
imports: BTreeMap::default(),
|
||||
extensions: HashMap::new(),
|
||||
|
||||
#[cfg(feature = "coverage")]
|
||||
coverage: HashMap::new(),
|
||||
#[cfg(feature = "coverage")]
|
||||
enable_coverage: false,
|
||||
|
||||
gather_prints: false,
|
||||
prints: Vec::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,6 +253,7 @@ impl Interpreter {
|
||||
pub fn clean_internal_evaluation_state(&mut self) {
|
||||
self.data = self.init_data.clone();
|
||||
self.processed.clear();
|
||||
self.processed_paths = Value::new_object();
|
||||
self.loop_var_values.clear();
|
||||
self.scopes = vec![Scope::new()];
|
||||
self.contexts = vec![];
|
||||
@@ -661,7 +681,13 @@ impl Interpreter {
|
||||
.map(Value::Bool);
|
||||
}
|
||||
// Treat the assignment as comparison if neither lhs nor rhs is a variable
|
||||
_ => return self.eval_bool_expr(&BoolOp::Eq, lhs, rhs),
|
||||
_ => {
|
||||
let r = self.eval_bool_expr(&BoolOp::Eq, lhs, rhs)?;
|
||||
if r == Value::Bool(false) {
|
||||
return Ok(Value::Undefined);
|
||||
}
|
||||
return Ok(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
AssignOp::ColEq => {
|
||||
@@ -1173,6 +1199,7 @@ impl Interpreter {
|
||||
let rule_values = self.rule_values.clone();
|
||||
|
||||
self.processed.clear();
|
||||
let processed_paths = std::mem::replace(&mut self.processed_paths, Value::new_object());
|
||||
self.rule_values.clear();
|
||||
|
||||
let mut skip_exec = false;
|
||||
@@ -1269,6 +1296,7 @@ impl Interpreter {
|
||||
input,
|
||||
data,
|
||||
processed,
|
||||
processed_paths,
|
||||
with_functions,
|
||||
rule_values,
|
||||
)),
|
||||
@@ -1286,6 +1314,7 @@ impl Interpreter {
|
||||
self.input,
|
||||
self.data,
|
||||
self.processed,
|
||||
self.processed_paths,
|
||||
self.with_functions,
|
||||
self.rule_values,
|
||||
) = s;
|
||||
@@ -1702,11 +1731,12 @@ impl Interpreter {
|
||||
.insert(Value::String(name.to_string().into()), value.clone());
|
||||
}
|
||||
}
|
||||
if result
|
||||
.expressions
|
||||
.iter()
|
||||
.all(|v| v.value != Value::Undefined && v.value != Value::Bool(false))
|
||||
&& !result.expressions.is_empty()
|
||||
if result.expressions.len() == 1 // Single expression query
|
||||
|| result // Multi expression query where no value is false
|
||||
.expressions
|
||||
.iter()
|
||||
.all(|v| v.value != Value::Undefined && v.value != Value::Bool(false))
|
||||
&& !result.expressions.is_empty()
|
||||
{
|
||||
ctx.results.result.push(result);
|
||||
}
|
||||
@@ -1821,7 +1851,9 @@ impl Interpreter {
|
||||
.insert(Value::String(name.to_string().into()), value.clone());
|
||||
}
|
||||
}
|
||||
if result
|
||||
|
||||
if result.expressions.len() == 1 // Single expression query
|
||||
|| result // Multi expression query where no value is false
|
||||
.expressions
|
||||
.iter()
|
||||
.all(|v| v.value != Value::Undefined && v.value != Value::Bool(false))
|
||||
@@ -1879,7 +1911,13 @@ impl Interpreter {
|
||||
// ( scalar | ref | var ) ":" term, the OPA
|
||||
// implementation is more like expr ":" expr
|
||||
let key = self.eval_expr(key)?;
|
||||
if key == Value::Undefined {
|
||||
return Ok(Value::Undefined);
|
||||
}
|
||||
let value = self.eval_expr(value)?;
|
||||
if value == Value::Undefined {
|
||||
return Ok(Value::Undefined);
|
||||
}
|
||||
object.insert(key, value);
|
||||
}
|
||||
|
||||
@@ -2020,7 +2058,8 @@ impl Interpreter {
|
||||
params: &[ExprRef],
|
||||
) -> Result<Value> {
|
||||
let mut args = vec![];
|
||||
let allow_undefined = name == "print"; // TODO: with modifier
|
||||
let is_print = name == "print"; // TODO: with modifier
|
||||
let allow_undefined = is_print;
|
||||
for p in params {
|
||||
match self.eval_expr(p)? {
|
||||
// If any argument is undefined, then the call is undefined.
|
||||
@@ -2029,6 +2068,17 @@ impl Interpreter {
|
||||
}
|
||||
}
|
||||
|
||||
if is_print && self.gather_prints {
|
||||
// Do not print to stderr. Instead, gather.
|
||||
let msg =
|
||||
builtins::print_to_string(span, params, &args[..], self.strict_builtin_errors)?;
|
||||
|
||||
// Prefix location information.
|
||||
self.prints
|
||||
.push(format!("{}:{}: {msg}", span.source.file(), span.line));
|
||||
return Ok(Value::Bool(true));
|
||||
}
|
||||
|
||||
let cache = builtins::must_cache(name);
|
||||
if let Some(name) = &cache {
|
||||
if let Some(v) = self.builtins_cache.get(&(name, args.clone())) {
|
||||
@@ -2036,7 +2086,12 @@ impl Interpreter {
|
||||
}
|
||||
}
|
||||
|
||||
let v = builtin.0(span, params, &args[..], self.strict_builtin_errors)?;
|
||||
let v = match builtin.0(span, params, &args[..], self.strict_builtin_errors) {
|
||||
Ok(v) => v,
|
||||
// Ignore errors if we are not evaluating in strict mode.
|
||||
Err(_) if !self.strict_builtin_errors => return Ok(Value::Undefined),
|
||||
Err(e) => Err(e)?,
|
||||
};
|
||||
|
||||
// Handle trace function.
|
||||
// TODO: with modifier.
|
||||
@@ -2166,7 +2221,7 @@ impl Interpreter {
|
||||
if param_values.len() != *nargs as usize {
|
||||
bail!(span.error("incorrect number of parameters supplied to extension"));
|
||||
}
|
||||
let r = ext(param_values);
|
||||
let r = Rc::make_mut(ext)(param_values);
|
||||
// Restore with_functions.
|
||||
if let Some(with_functions) = with_functions_saved {
|
||||
self.with_functions = with_functions;
|
||||
@@ -2400,13 +2455,11 @@ impl Interpreter {
|
||||
|| &module_path[path.len()..path.len() + 1] == ".")
|
||||
{
|
||||
// Ensure that the module is created.
|
||||
{
|
||||
let path = Parser::get_path_ref_components(&module.package.refr)?;
|
||||
let path: Vec<&str> = path.iter().map(|s| s.text()).collect();
|
||||
let vref = Self::make_or_get_value_mut(&mut self.data, &path[..])?;
|
||||
if *vref == Value::Undefined {
|
||||
*vref = Value::new_object();
|
||||
}
|
||||
let path = Parser::get_path_ref_components(&module.package.refr)?;
|
||||
let path: Vec<&str> = path.iter().map(|s| s.text()).collect();
|
||||
let vref = Self::make_or_get_value_mut(&mut self.data, &path[..])?;
|
||||
if *vref == Value::Undefined {
|
||||
*vref = Value::new_object();
|
||||
}
|
||||
|
||||
for rule in &module.policy {
|
||||
@@ -2422,13 +2475,17 @@ impl Interpreter {
|
||||
}
|
||||
}
|
||||
self.set_current_module(prev_module)?;
|
||||
self.mark_processed(&path)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_rule_evaluated(&mut self, path: String) -> Result<()> {
|
||||
let mut matched = false;
|
||||
if let Some(rules) = self.rules.get(&path) {
|
||||
matched = true;
|
||||
for r in rules.clone() {
|
||||
if !self.processed.contains(&r) {
|
||||
let module = self.get_rule_module(&r)?;
|
||||
@@ -2436,8 +2493,10 @@ impl Interpreter {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Evaluate the associated default rules after non-default rules
|
||||
if let Some(rules) = self.default_rules.get(&path) {
|
||||
matched = true;
|
||||
for (r, _) in rules.clone() {
|
||||
if !self.processed.contains(&r) {
|
||||
let module = self.get_rule_module(&r)?;
|
||||
@@ -2448,6 +2507,37 @@ impl Interpreter {
|
||||
}
|
||||
}
|
||||
|
||||
if matched {
|
||||
let comps: Vec<&str> = path.split('.').collect();
|
||||
self.mark_processed(&comps[1..])?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_processed(&self, path: &[&str]) -> Result<bool> {
|
||||
let mut obj = &self.processed_paths;
|
||||
for p in path {
|
||||
// Prefix has already been processed.
|
||||
if obj[&Value::Undefined] == Value::Null {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
match &obj[*p] {
|
||||
// Prefix and its suffixes including path have not been processed.
|
||||
Value::Undefined => return Ok(false),
|
||||
v => obj = v,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(obj[&Value::Undefined] == Value::Null)
|
||||
}
|
||||
|
||||
fn mark_processed(&mut self, path: &[&str]) -> Result<()> {
|
||||
let obj = self.processed_paths.make_or_get_value_mut(path)?;
|
||||
if obj == &Value::Undefined {
|
||||
*obj = Value::new_object();
|
||||
}
|
||||
obj.as_object_mut()?.insert(Value::Undefined, Value::Null);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2475,50 +2565,41 @@ impl Interpreter {
|
||||
|
||||
// Ensure that rules are evaluated
|
||||
if name.text() == "data" {
|
||||
if self.is_processed(fields)? {
|
||||
return Ok(Self::get_value_chained(self.data.clone(), fields));
|
||||
}
|
||||
|
||||
// If "data" is used in a query, without any fields, then evaluate all the modules.
|
||||
if fields.is_empty() && self.active_rules.is_empty() {
|
||||
for module in self.modules.clone() {
|
||||
for rule in &module.policy {
|
||||
self.eval_rule(&module, rule)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// With modifiers may be used to specify part of a module that that not yet been
|
||||
// evaluated. Therefore ensure that module is evaluated first.
|
||||
let path = "data.".to_owned() + &fields.join(".");
|
||||
self.ensure_module_evaluated(path)?;
|
||||
self.ensure_module_evaluated(path.clone())?;
|
||||
|
||||
// If the rule has already been evaluated or specified via a with modifier,
|
||||
// use that value.
|
||||
let v = Self::get_value_chained(self.data.clone(), fields);
|
||||
|
||||
if v != Value::Undefined {
|
||||
debug!("returning v = {v}");
|
||||
return Ok(v);
|
||||
}
|
||||
|
||||
// Find the rule to which the var being looked up corresponds to. This is the prefix for
|
||||
// which rules exist.
|
||||
let mut found = false;
|
||||
for i in (1..fields.len() + 1).rev() {
|
||||
let path = "data.".to_owned() + &fields[0..i].join(".");
|
||||
if self.rules.get(&path).is_some() || self.default_rules.get(&path).is_some() {
|
||||
self.ensure_rule_evaluated(path)?;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
// This could be path to a module.
|
||||
let path = "data.".to_owned() + &fields.join(".");
|
||||
self.ensure_module_evaluated(path)?;
|
||||
}
|
||||
|
||||
Ok(Self::get_value_chained(self.data.clone(), fields))
|
||||
} else if !self.modules.is_empty() {
|
||||
let path = Parser::get_path_ref_components(&self.module.clone().unwrap().package.refr)?;
|
||||
let mut path: Vec<&str> = path.iter().map(|s| s.text()).collect();
|
||||
path.push(name.text());
|
||||
|
||||
let v = Self::get_value_chained(self.data.clone(), &path);
|
||||
|
||||
// If the rule has already been evaluated or specified via a with modifier,
|
||||
// use that value.
|
||||
if v != Value::Undefined {
|
||||
return Ok(Self::get_value_chained(v, fields));
|
||||
if self.is_processed(&path)? {
|
||||
let value = Self::get_value_chained(self.data.clone(), &path);
|
||||
return Ok(Self::get_value_chained(value, fields));
|
||||
}
|
||||
|
||||
// Ensure that all the rules having common prefix (name) are evaluated.
|
||||
@@ -2574,6 +2655,31 @@ impl Interpreter {
|
||||
expr.span().text()
|
||||
);
|
||||
|
||||
#[cfg(feature = "coverage")]
|
||||
if self.enable_coverage {
|
||||
let span = expr.span();
|
||||
let source = &span.source;
|
||||
let line = span.line as usize;
|
||||
if line > 0 {
|
||||
// Check if coverage table already exists for source.
|
||||
match self.coverage.get_mut(source) {
|
||||
Some(c) => {
|
||||
// Ensure that current line is valid.
|
||||
if c.len() < line + 1 {
|
||||
c.resize(line + 1, false);
|
||||
}
|
||||
c[line] = true;
|
||||
}
|
||||
_ => {
|
||||
// Create new table.
|
||||
let mut c = vec![false; line + 1];
|
||||
c[line] = true;
|
||||
self.coverage.insert(source.clone(), c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match expr.as_ref() {
|
||||
Expr::Null(_) => Ok(Value::Null),
|
||||
Expr::True(_) => Ok(Value::Bool(true)),
|
||||
@@ -2624,7 +2730,22 @@ impl Interpreter {
|
||||
key, value, query, ..
|
||||
} => self.eval_object_compr(key, value, query),
|
||||
Expr::SetCompr { term, query, .. } => self.eval_set_compr(term, query),
|
||||
Expr::UnaryExpr { .. } => unimplemented!("unar expr is umplemented"),
|
||||
Expr::UnaryExpr { span, expr: uexpr } => match uexpr.as_ref() {
|
||||
Expr::Number(_) if !uexpr.span().text().starts_with('-') => {
|
||||
builtins::numbers::arithmetic_operation(
|
||||
span,
|
||||
&ArithOp::Sub,
|
||||
expr,
|
||||
uexpr,
|
||||
Value::from(0),
|
||||
self.eval_expr(uexpr)?,
|
||||
self.strict_builtin_errors,
|
||||
)
|
||||
}
|
||||
_ => bail!(expr
|
||||
.span()
|
||||
.error("unary - can only be used with numeric literals")),
|
||||
},
|
||||
Expr::Call { span, fcn, params } => {
|
||||
self.eval_call(span, expr, fcn, params, None, false)
|
||||
}
|
||||
@@ -2805,25 +2926,23 @@ impl Interpreter {
|
||||
pub fn get_path_string(refr: &Expr, document: Option<&str>) -> Result<String> {
|
||||
let mut comps = vec![];
|
||||
let mut expr = Some(refr);
|
||||
while expr.is_some() {
|
||||
match expr {
|
||||
Some(Expr::RefDot { refr, field, .. }) => {
|
||||
while let Some(e) = expr {
|
||||
match e {
|
||||
Expr::RefDot { refr, field, .. } => {
|
||||
comps.push(field.text());
|
||||
expr = Some(refr);
|
||||
}
|
||||
Some(Expr::RefBrack { refr, index, .. })
|
||||
if matches!(index.as_ref(), Expr::String(_)) =>
|
||||
{
|
||||
Expr::RefBrack { refr, index, .. } if matches!(index.as_ref(), Expr::String(_)) => {
|
||||
if let Expr::String(s) = index.as_ref() {
|
||||
comps.push(s.text());
|
||||
expr = Some(refr);
|
||||
}
|
||||
}
|
||||
Some(Expr::Var(v)) => {
|
||||
Expr::Var(v) => {
|
||||
comps.push(v.text());
|
||||
expr = None;
|
||||
}
|
||||
_ => bail!(format!("internal error: not a simplee ref {expr:?}")),
|
||||
_ => bail!(e.span().error("invalid ref expression")),
|
||||
}
|
||||
}
|
||||
if let Some(d) = document {
|
||||
@@ -3242,7 +3361,8 @@ impl Interpreter {
|
||||
|
||||
if let Some(r) = results.result.last() {
|
||||
if matches!(&r.bindings, Value::Object(obj) if obj.is_empty())
|
||||
&& r.expressions.iter().any(|e| e.value == Value::Bool(false))
|
||||
&& (r.expressions.len() > 1
|
||||
&& r.expressions.iter().any(|e| e.value == Value::Bool(false)))
|
||||
{
|
||||
results = QueryResults::default();
|
||||
}
|
||||
@@ -3290,31 +3410,29 @@ impl Interpreter {
|
||||
debug!("processing module {module_path:?}");
|
||||
|
||||
for rule in &module.policy {
|
||||
let mut rule_refr = Self::get_rule_refr(rule);
|
||||
debug!("rule refr: {}", rule_refr.span().text());
|
||||
debug!("rule : {:?}", rule);
|
||||
if let Rule::Spec {
|
||||
head:
|
||||
RuleHead::Set {
|
||||
refr, key: None, ..
|
||||
},
|
||||
..
|
||||
} = rule.as_ref()
|
||||
{
|
||||
rule_refr = match refr.as_ref() {
|
||||
Expr::RefDot { refr, .. } => refr,
|
||||
|
||||
_ => refr,
|
||||
let rule_refr = Self::get_rule_refr(rule);
|
||||
let mut prefix_path = module_path.clone();
|
||||
let mut components = Self::get_rule_path_components(rule_refr)?;
|
||||
let is_old_set = matches!(
|
||||
rule.as_ref(),
|
||||
Rule::Spec {
|
||||
head: RuleHead::Set { key: None, .. },
|
||||
..
|
||||
}
|
||||
);
|
||||
|
||||
if components.len() >= 2 && is_old_set {
|
||||
components.pop();
|
||||
}
|
||||
|
||||
let mut prefix_path = module_path.clone();
|
||||
prefix_path.append(&mut Self::get_rule_path_components(rule_refr)?);
|
||||
let prefix_path: Vec<&str> = prefix_path[0..prefix_path.len() - 1]
|
||||
.iter()
|
||||
.map(|s| s.as_ref())
|
||||
.collect();
|
||||
if components.len() > 1 {
|
||||
components.pop();
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
prefix_path.append(&mut components);
|
||||
let prefix_path: Vec<&str> = prefix_path.iter().map(|s| s.as_ref()).collect();
|
||||
if Self::get_value_chained(self.data.clone(), &prefix_path) == Value::Undefined {
|
||||
self.update_data(
|
||||
rule_refr.span(),
|
||||
@@ -3348,6 +3466,42 @@ impl Interpreter {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn record_default_rule(
|
||||
&mut self,
|
||||
refr: &Ref<Expr>,
|
||||
rule: &Ref<Rule>,
|
||||
index: Option<String>,
|
||||
) -> Result<()> {
|
||||
let comps = Parser::get_path_ref_components(refr)?;
|
||||
let comps: Vec<&str> = comps.iter().map(|s| s.text()).collect();
|
||||
for (idx, c) in (0..comps.len()).enumerate() {
|
||||
let path = self.current_module_path.clone() + "." + &comps[0..c + 1].join(".");
|
||||
match self.default_rules.entry(path) {
|
||||
Entry::Occupied(o) => {
|
||||
if idx + 1 == comps.len() {
|
||||
for (_, i) in o.get() {
|
||||
if index.is_some() && i.is_some() {
|
||||
let old = i.as_ref().unwrap();
|
||||
let new = index.as_ref().unwrap();
|
||||
if old == new {
|
||||
bail!(refr.span().error("multiple default rules for the variable with the same index"));
|
||||
}
|
||||
} else if index.is_some() || i.is_some() {
|
||||
bail!(refr.span().error("conflict type with the default rules"));
|
||||
}
|
||||
}
|
||||
}
|
||||
o.into_mut().push((rule.clone(), index.clone()));
|
||||
}
|
||||
Entry::Vacant(v) => {
|
||||
v.insert(vec![(rule.clone(), index.clone())]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn process_imports(&mut self) -> Result<()> {
|
||||
for module in &self.modules {
|
||||
let module_path = get_path_string(&module.package.refr, Some("data"))?;
|
||||
@@ -3360,11 +3514,25 @@ impl Interpreter {
|
||||
Expr::String(s) => s.text(),
|
||||
_ => "",
|
||||
},
|
||||
Expr::Var(v) if v.text() == "input" => {
|
||||
// Warn redundant import of input. Ignore it.
|
||||
eprintln!(
|
||||
"{}",
|
||||
import
|
||||
.refr
|
||||
.span()
|
||||
.message("warning", "redundant import of `input`")
|
||||
);
|
||||
continue;
|
||||
}
|
||||
_ => "",
|
||||
},
|
||||
};
|
||||
if target.is_empty() {
|
||||
bail!(import.refr.span().error("invalid ref in import"));
|
||||
bail!(import
|
||||
.refr
|
||||
.span()
|
||||
.message("warning", "invalid ref in import"));
|
||||
}
|
||||
self.imports
|
||||
.insert(module_path.clone() + "." + target, import.refr.clone());
|
||||
@@ -3411,29 +3579,7 @@ impl Interpreter {
|
||||
_ => (refr, None),
|
||||
};
|
||||
|
||||
let path = Self::get_path_string(refr, None)?;
|
||||
let path = self.current_module_path.clone() + "." + &path;
|
||||
match self.default_rules.entry(path) {
|
||||
Entry::Occupied(o) => {
|
||||
for (_, i) in o.get() {
|
||||
if index.is_some() && i.is_some() {
|
||||
let old = i.as_ref().unwrap();
|
||||
let new = index.as_ref().unwrap();
|
||||
if old == new {
|
||||
bail!(refr.span().error("multiple default rules for the variable with the same index"));
|
||||
}
|
||||
} else if index.is_some() || i.is_some() {
|
||||
bail!(refr
|
||||
.span()
|
||||
.error("conflict type with the default rules"));
|
||||
}
|
||||
}
|
||||
o.into_mut().push((rule.clone(), index));
|
||||
}
|
||||
Entry::Vacant(v) => {
|
||||
v.insert(vec![(rule.clone(), index)]);
|
||||
}
|
||||
}
|
||||
self.record_default_rule(refr, rule, index)?;
|
||||
}
|
||||
}
|
||||
self.set_current_module(prev_module)?;
|
||||
@@ -3448,10 +3594,143 @@ impl Interpreter {
|
||||
extension: Box<dyn Extension>,
|
||||
) -> Result<()> {
|
||||
if let std::collections::hash_map::Entry::Vacant(v) = self.extensions.entry(path) {
|
||||
v.insert((nargs, extension));
|
||||
v.insert((nargs, Rc::new(extension)));
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("extension already added");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "coverage")]
|
||||
fn gather_coverage_in_query(
|
||||
&self,
|
||||
query: &Ref<Query>,
|
||||
covered: &Vec<bool>,
|
||||
file: &mut crate::coverage::File,
|
||||
) -> Result<()> {
|
||||
for stmt in &query.stmts {
|
||||
// TODO: with mods
|
||||
match &stmt.literal {
|
||||
Literal::SomeVars { .. } => (),
|
||||
Literal::SomeIn {
|
||||
value, collection, ..
|
||||
} => {
|
||||
self.gather_coverage_in_expr(value, covered, file)?;
|
||||
self.gather_coverage_in_expr(collection, covered, file)?;
|
||||
}
|
||||
Literal::Expr { expr, .. } | Literal::NotExpr { expr, .. } => {
|
||||
self.gather_coverage_in_expr(expr, covered, file)?;
|
||||
}
|
||||
Literal::Every { domain, query, .. } => {
|
||||
self.gather_coverage_in_expr(domain, covered, file)?;
|
||||
self.gather_coverage_in_query(query, covered, file)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "coverage")]
|
||||
fn gather_coverage_in_expr(
|
||||
&self,
|
||||
expr: &Ref<Expr>,
|
||||
covered: &Vec<bool>,
|
||||
file: &mut crate::coverage::File,
|
||||
) -> Result<()> {
|
||||
use Expr::*;
|
||||
traverse(expr, &mut |e| {
|
||||
Ok(match e.as_ref() {
|
||||
ArrayCompr { query, .. } | SetCompr { query, .. } | ObjectCompr { query, .. } => {
|
||||
self.gather_coverage_in_query(query, covered, file)?;
|
||||
false
|
||||
}
|
||||
_ => {
|
||||
let line = e.span().line as usize;
|
||||
if line >= covered.len() || !covered[line] {
|
||||
file.not_covered.insert(line as u32);
|
||||
} else if line < covered.len() && covered[line] {
|
||||
file.covered.insert(line as u32);
|
||||
}
|
||||
true
|
||||
}
|
||||
})
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "coverage")]
|
||||
pub fn get_coverage_report(&self) -> Result<crate::coverage::Report> {
|
||||
let mut report = crate::coverage::Report::default();
|
||||
|
||||
for module in self.modules.iter() {
|
||||
let span = module.package.refr.span();
|
||||
|
||||
// Get coverage information for the module.
|
||||
let Some(covered) = self.coverage.get(&span.source) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut file = crate::coverage::File {
|
||||
path: span.source.file().clone(),
|
||||
code: span.source.contents().clone(),
|
||||
covered: BTreeSet::new(),
|
||||
not_covered: BTreeSet::new(),
|
||||
};
|
||||
|
||||
// Loop through each rule and figure out the lines that were not coverd.
|
||||
for rule in &module.policy {
|
||||
match rule.as_ref() {
|
||||
Rule::Spec { head, bodies, .. } => {
|
||||
match head {
|
||||
RuleHead::Compr { assign, .. } | RuleHead::Func { assign, .. } => {
|
||||
if let Some(a) = assign {
|
||||
self.gather_coverage_in_expr(&a.value, covered, &mut file)?;
|
||||
}
|
||||
}
|
||||
RuleHead::Set { key, .. } => {
|
||||
if let Some(k) = key {
|
||||
self.gather_coverage_in_expr(k, covered, &mut file)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
for b in bodies {
|
||||
self.gather_coverage_in_query(&b.query, covered, &mut file)?;
|
||||
}
|
||||
}
|
||||
Rule::Default { value, .. } => {
|
||||
self.gather_coverage_in_expr(value, covered, &mut file)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
report.files.push(file);
|
||||
}
|
||||
|
||||
Ok(report)
|
||||
}
|
||||
|
||||
#[cfg(feature = "coverage")]
|
||||
pub fn set_enable_coverage(&mut self, enable: bool) {
|
||||
if self.enable_coverage != enable {
|
||||
self.enable_coverage = enable;
|
||||
self.clear_coverage_data();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "coverage")]
|
||||
pub fn clear_coverage_data(&mut self) {
|
||||
self.coverage = HashMap::new();
|
||||
}
|
||||
|
||||
pub fn set_gather_prints(&mut self, b: bool) {
|
||||
if b != self.gather_prints {
|
||||
// Clear existing prints.
|
||||
std::mem::take(&mut self.prints);
|
||||
}
|
||||
self.gather_prints = b;
|
||||
}
|
||||
|
||||
pub fn take_prints(&mut self) -> Result<Vec<String>> {
|
||||
Ok(std::mem::take(&mut self.prints))
|
||||
}
|
||||
}
|
||||
|
||||
41
src/lexer.rs
41
src/lexer.rs
@@ -6,9 +6,12 @@ use core::iter::Peekable;
|
||||
use core::str::CharIndices;
|
||||
|
||||
use std::convert::AsRef;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::path::Path;
|
||||
|
||||
use crate::value::Value;
|
||||
use crate::Rc;
|
||||
use crate::Value;
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -20,7 +23,39 @@ struct SourceInternal {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Source {
|
||||
src: std::rc::Rc<SourceInternal>,
|
||||
src: Rc<SourceInternal>,
|
||||
}
|
||||
|
||||
impl std::cmp::Ord for Source {
|
||||
fn cmp(&self, other: &Source) -> std::cmp::Ordering {
|
||||
Rc::as_ptr(&self.src).cmp(&Rc::as_ptr(&other.src))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::cmp::PartialOrd for Source {
|
||||
fn partial_cmp(&self, other: &Source) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::cmp::PartialEq for Source {
|
||||
fn eq(&self, other: &Source) -> bool {
|
||||
Rc::as_ptr(&self.src) == Rc::as_ptr(&other.src)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::cmp::Eq for Source {}
|
||||
|
||||
impl Hash for Source {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
Rc::as_ptr(&self.src).hash(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Source {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||
self.src.file.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -108,7 +143,7 @@ impl Source {
|
||||
lines.push((s, s));
|
||||
}
|
||||
Self {
|
||||
src: std::rc::Rc::new(SourceInternal {
|
||||
src: Rc::new(SourceInternal {
|
||||
file,
|
||||
contents,
|
||||
lines,
|
||||
|
||||
132
src/lib.rs
132
src/lib.rs
@@ -3,6 +3,7 @@
|
||||
|
||||
// Use README.md as crate documentation.
|
||||
#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
@@ -20,6 +21,12 @@ mod value;
|
||||
pub use engine::Engine;
|
||||
pub use value::Value;
|
||||
|
||||
#[cfg(feature = "arc")]
|
||||
use std::sync::Arc as Rc;
|
||||
|
||||
#[cfg(not(feature = "arc"))]
|
||||
use std::rc::Rc;
|
||||
|
||||
/// Location of an [`Expression`] in a Rego query.
|
||||
///
|
||||
/// ```
|
||||
@@ -37,7 +44,7 @@ pub use value::Value;
|
||||
/// # }
|
||||
/// ````
|
||||
/// See also [`QueryResult`].
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[derive(Debug, Clone, Serialize, Eq, PartialEq)]
|
||||
pub struct Location {
|
||||
/// Line number. Starts at 1.
|
||||
pub row: u16,
|
||||
@@ -62,13 +69,13 @@ pub struct Location {
|
||||
/// # }
|
||||
/// ```
|
||||
/// See also [`QueryResult`].
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[derive(Debug, Clone, Serialize, Eq, PartialEq)]
|
||||
pub struct Expression {
|
||||
/// Computed value of the expression.
|
||||
pub value: Value,
|
||||
|
||||
/// The Rego expression.
|
||||
pub text: std::rc::Rc<str>,
|
||||
pub text: Rc<str>,
|
||||
|
||||
/// Location of the expression in the query string.
|
||||
pub location: Location,
|
||||
@@ -150,7 +157,7 @@ pub struct Expression {
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[derive(Debug, Clone, Serialize, Eq, PartialEq)]
|
||||
pub struct QueryResult {
|
||||
/// Expressions in the query.
|
||||
///
|
||||
@@ -180,17 +187,33 @@ impl Default for QueryResult {
|
||||
/// ```
|
||||
/// # use regorus::*;
|
||||
/// # fn main() -> anyhow::Result<()> {
|
||||
/// // Create engine and evaluate "true; true; false".
|
||||
/// // Create engine and evaluate "1 + 1".
|
||||
/// let results = Engine::new().eval_query("1 + 1".to_string(), false)?;
|
||||
///
|
||||
/// assert!(results.result.len() == 1);
|
||||
/// assert_eq!(results.result.len(), 1);
|
||||
/// assert_eq!(results.result[0].expressions[0].value, Value::from(2u64));
|
||||
/// assert_eq!(results.result[0].expressions[0].text.as_ref(), "1 + 1");
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// If any expression evaluates to false, then no results are produced.
|
||||
/// If a query contains only one expression, and even if the expression evaluates
|
||||
/// to false, the value will be returned.
|
||||
/// ```
|
||||
/// # use regorus::*;
|
||||
/// # fn main() -> anyhow::Result<()> {
|
||||
/// // Create engine and evaluate "1 > 2" which is false.
|
||||
/// let results = Engine::new().eval_query("1 > 2".to_string(), false)?;
|
||||
///
|
||||
/// assert_eq!(results.result.len(), 1);
|
||||
/// assert_eq!(results.result[0].expressions[0].value, Value::from(false));
|
||||
/// assert_eq!(results.result[0].expressions[0].text.as_ref(), "1 > 2");
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// In a query containing multiple expressions, if any expression evaluates to false,
|
||||
/// then no results are produced.
|
||||
/// ```
|
||||
/// # use regorus::*;
|
||||
/// # fn main() -> anyhow::Result<()> {
|
||||
@@ -202,6 +225,26 @@ impl Default for QueryResult {
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// Note that `=` is different from `==`. The former evaluates to undefined if the LHS and RHS
|
||||
/// are not equal. The latter evaluates to either true or false.
|
||||
/// ```
|
||||
/// # use regorus::*;
|
||||
/// # fn main() -> anyhow::Result<()> {
|
||||
/// // Create engine and evaluate "1 = 2" which is undefined and produces no resutl.
|
||||
/// let results = Engine::new().eval_query("1 = 2".to_string(), false)?;
|
||||
///
|
||||
/// assert_eq!(results.result.len(), 0);
|
||||
///
|
||||
/// // Create engine and evaluate "1 == 2" which evaluates to false.
|
||||
/// let results = Engine::new().eval_query("1 == 2".to_string(), false)?;
|
||||
///
|
||||
/// assert_eq!(results.result.len(), 1);
|
||||
/// assert_eq!(results.result[0].expressions[0].value, Value::from(false));
|
||||
/// assert_eq!(results.result[0].expressions[0].text.as_ref(), "1 == 2");
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// Queries containing loops produce multiple results.
|
||||
/// ```
|
||||
/// # use regorus::*;
|
||||
@@ -253,7 +296,7 @@ impl Default for QueryResult {
|
||||
/// ```
|
||||
///
|
||||
/// See [QueryResult] for examples of different kinds of results.
|
||||
#[derive(Debug, Clone, Default, Serialize)]
|
||||
#[derive(Debug, Clone, Default, Serialize, Eq, PartialEq)]
|
||||
pub struct QueryResults {
|
||||
/// Collection of results of evaluting a query.
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
@@ -263,7 +306,7 @@ pub struct QueryResults {
|
||||
/// A user defined builtin function implementation.
|
||||
///
|
||||
/// It is not necessary to implement this trait directly.
|
||||
pub trait Extension: FnMut(Vec<Value>) -> anyhow::Result<Value> {
|
||||
pub trait Extension: FnMut(Vec<Value>) -> anyhow::Result<Value> + Send + Sync {
|
||||
/// Fn, FnMut etc are not sized and cannot be cloned in their boxed form.
|
||||
/// clone_box exists to overcome that.
|
||||
fn clone_box<'a>(&self) -> Box<dyn 'a + Extension>
|
||||
@@ -274,7 +317,7 @@ pub trait Extension: FnMut(Vec<Value>) -> anyhow::Result<Value> {
|
||||
/// Automatically make matching closures a valid [`Extension`].
|
||||
impl<F> Extension for F
|
||||
where
|
||||
F: FnMut(Vec<Value>) -> anyhow::Result<Value> + Clone,
|
||||
F: FnMut(Vec<Value>) -> anyhow::Result<Value> + Clone + Send + Sync,
|
||||
{
|
||||
fn clone_box<'a>(&self) -> Box<dyn 'a + Extension>
|
||||
where
|
||||
@@ -291,6 +334,75 @@ impl<'a> Clone for Box<dyn 'a + Extension> {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for dyn Extension {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||
f.write_fmt(format_args!("<extension>"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "coverage")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "coverage")))]
|
||||
pub mod coverage {
|
||||
#[derive(Default, serde::Serialize, serde::Deserialize)]
|
||||
/// Coverage information about a rego policy file.
|
||||
pub struct File {
|
||||
/// Path of the policy file.
|
||||
pub path: String,
|
||||
|
||||
/// The rego policy.
|
||||
pub code: String,
|
||||
|
||||
/// Lines that were evaluated.
|
||||
pub covered: std::collections::BTreeSet<u32>,
|
||||
|
||||
/// Lines that were not evaluated.
|
||||
pub not_covered: std::collections::BTreeSet<u32>,
|
||||
}
|
||||
|
||||
#[derive(Default, serde::Serialize, serde::Deserialize)]
|
||||
/// Policy coverage report.
|
||||
pub struct Report {
|
||||
/// Coverage information for files.
|
||||
pub files: Vec<File>,
|
||||
}
|
||||
|
||||
impl Report {
|
||||
/// Produce an ANSI color encoded version of the report.
|
||||
///
|
||||
/// Covered lines are green.
|
||||
/// Lines that are not covered are red.
|
||||
///
|
||||
/// <img src="https://github.com/microsoft/regorus/blob/main/docs/coverage.png?raw=true">
|
||||
|
||||
pub fn to_colored_string(&self) -> anyhow::Result<String> {
|
||||
use std::io::Write;
|
||||
let mut s = Vec::new();
|
||||
writeln!(&mut s, "COVERAGE REPORT:")?;
|
||||
for file in self.files.iter() {
|
||||
if file.not_covered.is_empty() {
|
||||
writeln!(&mut s, "{} has full coverage", file.path)?;
|
||||
continue;
|
||||
}
|
||||
|
||||
writeln!(&mut s, "{}:", file.path)?;
|
||||
for (line, code) in file.code.split('\n').enumerate() {
|
||||
let line = line as u32 + 1;
|
||||
if file.not_covered.contains(&line) {
|
||||
writeln!(&mut s, "\x1b[31m {line:4} {code}\x1b[0m")?;
|
||||
} else if file.covered.contains(&line) {
|
||||
writeln!(&mut s, "\x1b[32m {line:4} {code}\x1b[0m")?;
|
||||
} else {
|
||||
writeln!(&mut s, " {line:4} {code}")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(&mut s)?;
|
||||
Ok(std::str::from_utf8(&s)?.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Items in `unstable` are likely to change.
|
||||
#[doc(hidden)]
|
||||
pub mod unstable {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
use core::fmt::{Debug, Formatter};
|
||||
use std::cmp::{Ord, Ordering};
|
||||
use std::rc::Rc;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
@@ -11,6 +10,8 @@ use anyhow::{anyhow, bail, Result};
|
||||
use serde::ser::Serializer;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::Rc;
|
||||
|
||||
pub type BigInt = i128;
|
||||
|
||||
type BigFloat = scientific::Scientific;
|
||||
|
||||
@@ -1426,7 +1426,21 @@ impl<'source> Parser<'source> {
|
||||
"expected assignment or query after `else`",
|
||||
));
|
||||
}
|
||||
_ => break,
|
||||
_ => {
|
||||
let mut query_span = span.clone();
|
||||
query_span.end = query_span.start;
|
||||
let query = Ref::new(Query {
|
||||
span: query_span,
|
||||
stmts: vec![],
|
||||
});
|
||||
span.end = self.end;
|
||||
bodies.push(RuleBody {
|
||||
span,
|
||||
assign,
|
||||
query,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -380,7 +380,7 @@ pub struct Analyzer {
|
||||
current_module_path: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Schedule {
|
||||
pub scopes: BTreeMap<Ref<Query>, Scope>,
|
||||
pub order: BTreeMap<Ref<Query>, Vec<u16>>,
|
||||
|
||||
@@ -143,6 +143,9 @@ pub fn eval_file(
|
||||
let mut engine: Engine = Engine::new();
|
||||
engine.set_strict_builtin_errors(strict);
|
||||
|
||||
#[cfg(feature = "coverage")]
|
||||
engine.set_enable_coverage(true);
|
||||
|
||||
let mut results = vec![];
|
||||
let mut files = vec![];
|
||||
|
||||
@@ -159,31 +162,48 @@ pub fn eval_file(
|
||||
engine.add_data(data)?;
|
||||
}
|
||||
|
||||
if let Some(input) = input_opt {
|
||||
// all modules are evaluated for each input
|
||||
let mut inputs = vec![];
|
||||
match input {
|
||||
ValueOrVec::Single(single_input) => inputs.push(single_input),
|
||||
ValueOrVec::Many(mut many_input) => inputs.append(&mut many_input),
|
||||
let mut inputs = vec![];
|
||||
match input_opt {
|
||||
Some(ValueOrVec::Single(single_input)) => inputs.push(single_input),
|
||||
Some(ValueOrVec::Many(mut many_input)) => inputs.append(&mut many_input),
|
||||
_ => (),
|
||||
}
|
||||
|
||||
let mut engine_full = engine.clone();
|
||||
|
||||
if inputs.is_empty() {
|
||||
// Now eval the query.
|
||||
let r = engine.eval_query(query.to_string(), enable_tracing)?;
|
||||
let r_full = engine_full.eval_query_and_all_rules(query.to_string(), enable_tracing)?;
|
||||
if r != r_full {
|
||||
println!(
|
||||
"{}\n{}",
|
||||
serde_json::to_string_pretty(&r_full)?,
|
||||
serde_json::to_string_pretty(&r)?
|
||||
);
|
||||
assert_eq!(r_full, r);
|
||||
}
|
||||
|
||||
push_query_results(r, &mut results);
|
||||
} else {
|
||||
for input in inputs {
|
||||
engine.set_input(input);
|
||||
engine.eval_modules(enable_tracing)?;
|
||||
engine.set_input(input.clone());
|
||||
engine_full.set_input(input);
|
||||
|
||||
// Now eval the query.
|
||||
push_query_results(
|
||||
engine.eval_query(query.to_string(), enable_tracing)?,
|
||||
&mut results,
|
||||
);
|
||||
let r = engine.eval_query(query.to_string(), enable_tracing)?;
|
||||
let r_full = engine_full.eval_query_and_all_rules(query.to_string(), enable_tracing)?;
|
||||
if r != r_full {
|
||||
println!(
|
||||
"{}\n{}",
|
||||
serde_json::to_string_pretty(&r_full)?,
|
||||
serde_json::to_string_pretty(&r)?
|
||||
);
|
||||
assert_eq!(r_full, r);
|
||||
}
|
||||
|
||||
push_query_results(r, &mut results);
|
||||
}
|
||||
} else {
|
||||
// it no input is defined then one evaluation of all modules is performed
|
||||
// Now eval the query.
|
||||
push_query_results(
|
||||
engine.eval_query(query.to_string(), enable_tracing)?,
|
||||
&mut results,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
@@ -234,6 +254,7 @@ struct TestCase {
|
||||
query: String,
|
||||
sort_bindings: Option<bool>,
|
||||
want_result: Option<ValueOrVec>,
|
||||
no_result: Option<bool>,
|
||||
skip: Option<bool>,
|
||||
error: Option<String>,
|
||||
traces: Option<bool>,
|
||||
@@ -267,7 +288,10 @@ fn yaml_test_impl(file: &str) -> Result<()> {
|
||||
|
||||
match (&case.want_result, &case.error) {
|
||||
(Some(_), None) | (None, Some(_)) => (),
|
||||
_ => panic!("either want_result or error must be specified in test case."),
|
||||
_ if case.no_result != Some(true) => {
|
||||
panic!("either want_result, error or no_result must be specified in test case.")
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
let enable_tracing = case.traces.is_some() && case.traces.unwrap();
|
||||
@@ -292,6 +316,7 @@ fn yaml_test_impl(file: &str) -> Result<()> {
|
||||
|
||||
check_output(&results, &expected_results)?;
|
||||
}
|
||||
_ if case.no_result == Some(true) => (),
|
||||
_ => bail!("eval succeeded and did not produce any errors"),
|
||||
},
|
||||
Err(actual) => match &case.error {
|
||||
|
||||
@@ -190,11 +190,12 @@ pub fn gather_functions(modules: &[Ref<Module>]) -> Result<FunctionTable> {
|
||||
}
|
||||
|
||||
pub fn get_root_var(mut expr: &Expr) -> Result<SourceStr> {
|
||||
let empty = expr.span().source_str().clone_empty();
|
||||
loop {
|
||||
match expr {
|
||||
Expr::Var(v) => return Ok(v.source_str()),
|
||||
Expr::RefDot { refr, .. } | Expr::RefBrack { refr, .. } => expr = refr,
|
||||
_ => bail!("internal error: analyzer: could not get rule prefix"),
|
||||
_ => return Ok(empty),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::convert::AsRef;
|
||||
use std::ops;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
@@ -16,6 +15,8 @@ use serde::de::{self, Deserializer, MapAccess, SeqAccess, Visitor};
|
||||
use serde::ser::{SerializeMap, Serializer};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::Rc;
|
||||
|
||||
/// A value in a Rego document.
|
||||
///
|
||||
/// Value is similar to a [`serde_json::value::Value`], but has the following additional
|
||||
|
||||
@@ -41,8 +41,13 @@ fn eval_test_case(dir: &Path, case: &TestCase) -> Result<Value> {
|
||||
}
|
||||
}
|
||||
|
||||
let mut engine_full = engine.clone();
|
||||
let query_results = engine.eval_query(case.query.clone(), true)?;
|
||||
|
||||
// Ensure that full evaluation produces the same results.
|
||||
let query_results_full = engine_full.eval_query_and_all_rules(case.query.clone(), true)?;
|
||||
assert_eq!(query_results, query_results_full);
|
||||
|
||||
let mut values = vec![];
|
||||
for qr in query_results.result {
|
||||
values.push(if !qr.bindings.as_object()?.is_empty() {
|
||||
@@ -108,6 +113,55 @@ fn run_aci_tests(dir: &Path) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "coverage")]
|
||||
fn run_aci_tests_coverage(dir: &Path) -> Result<()> {
|
||||
let mut engine = Engine::new();
|
||||
engine.set_enable_coverage(true);
|
||||
|
||||
let mut added = std::collections::BTreeSet::new();
|
||||
|
||||
for entry in WalkDir::new(dir)
|
||||
.sort_by_file_name()
|
||||
.into_iter()
|
||||
.filter_map(|e| e.ok())
|
||||
{
|
||||
let path = entry.path();
|
||||
if !path.to_string_lossy().ends_with(".yaml") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let yaml = std::fs::read(&path)?;
|
||||
let yaml = String::from_utf8_lossy(&yaml);
|
||||
let test: YamlTest = serde_yaml::from_str(&yaml)?;
|
||||
|
||||
for case in &test.cases {
|
||||
for (idx, rego) in case.modules.iter().enumerate() {
|
||||
if rego.ends_with(".rego") {
|
||||
let path = dir.join(rego);
|
||||
let path = path.to_str().expect("not a valid path");
|
||||
let path = path.to_string();
|
||||
if !added.contains(&path) {
|
||||
engine.add_policy_from_file(path.to_string())?;
|
||||
added.insert(path);
|
||||
}
|
||||
} else {
|
||||
engine.add_policy(format!("rego{idx}.rego"), rego.clone())?;
|
||||
}
|
||||
}
|
||||
|
||||
engine.clear_data();
|
||||
engine.add_data(case.data.clone())?;
|
||||
engine.set_input(case.input.clone());
|
||||
let _query_results = engine.eval_query(case.query.clone(), true)?;
|
||||
}
|
||||
}
|
||||
|
||||
let report = engine.get_coverage_report()?;
|
||||
println!("{}", report.to_colored_string()?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(clap::Parser)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
struct Cli {
|
||||
@@ -119,5 +173,9 @@ struct Cli {
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
#[cfg(feature = "coverage")]
|
||||
run_aci_tests_coverage(&Path::new(&cli.test_dir))?;
|
||||
|
||||
run_aci_tests(&Path::new(&cli.test_dir))
|
||||
}
|
||||
|
||||
33
tests/arc.rs
Normal file
33
tests/arc.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use regorus::*;
|
||||
|
||||
// Ensure that types can be s
|
||||
lazy_static! {
|
||||
static ref VALUE: Value = Value::Null;
|
||||
static ref ENGINE: Mutex<Engine> = Mutex::new(Engine::new());
|
||||
// static ref ENGINE: Engine = Engine::new();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_engine() -> anyhow::Result<()> {
|
||||
let e_guard = ENGINE.lock();
|
||||
let mut engine = e_guard.expect("failed to lock engine");
|
||||
|
||||
engine.add_policy(
|
||||
"hello.rego".to_string(),
|
||||
r#"
|
||||
package test
|
||||
allow = true
|
||||
"#
|
||||
.to_string(),
|
||||
)?;
|
||||
|
||||
let results = engine.eval_query("data.test.allow".to_string(), false)?;
|
||||
assert_eq!(results.result[0].expressions[0].value, Value::from(true));
|
||||
Ok(())
|
||||
}
|
||||
89
tests/coverage/mod.rs
Normal file
89
tests/coverage/mod.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use regorus::*;
|
||||
|
||||
use anyhow::Result;
|
||||
use test_generator::test_resources;
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct File {
|
||||
covered: BTreeSet<u32>,
|
||||
not_covered: BTreeSet<u32>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct TestCase {
|
||||
data: Option<Value>,
|
||||
input: Option<Value>,
|
||||
modules: Vec<String>,
|
||||
note: String,
|
||||
query: String,
|
||||
skip: Option<bool>,
|
||||
report: Vec<File>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct YamlTest {
|
||||
cases: Vec<TestCase>,
|
||||
}
|
||||
|
||||
fn yaml_test_impl(file: &str) -> Result<()> {
|
||||
let yaml_str = std::fs::read_to_string(file)?;
|
||||
let test: YamlTest = serde_yaml::from_str(&yaml_str)?;
|
||||
|
||||
println!("running {file}");
|
||||
|
||||
for case in test.cases.into_iter() {
|
||||
print!("case {} ", case.note);
|
||||
if case.skip == Some(true) {
|
||||
println!("skipped");
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut engine = Engine::new();
|
||||
engine.set_enable_coverage(true);
|
||||
|
||||
for (idx, rego) in case.modules.iter().enumerate() {
|
||||
engine.add_policy(format!("rego_{idx}"), rego.clone())?;
|
||||
}
|
||||
|
||||
if let Some(data) = case.data {
|
||||
engine.add_data(data)?;
|
||||
}
|
||||
|
||||
if let Some(input) = case.input {
|
||||
engine.set_input(input);
|
||||
}
|
||||
|
||||
let _ = engine.eval_query(case.query.clone(), false)?;
|
||||
let report = engine.get_coverage_report()?;
|
||||
|
||||
for (idx, file) in case.report.into_iter().enumerate() {
|
||||
assert_eq!(file.not_covered, report.files[idx].not_covered);
|
||||
assert_eq!(file.covered, report.files[idx].covered);
|
||||
}
|
||||
|
||||
println!("passed");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn yaml_test(file: &str) -> Result<()> {
|
||||
match yaml_test_impl(file) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => {
|
||||
// If Err is returned, it doesn't always get printed by cargo test.
|
||||
// Therefore, panic with the error.
|
||||
panic!("{}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test_resources("tests/coverage/*.yaml")]
|
||||
fn run(path: &str) {
|
||||
yaml_test(path).unwrap()
|
||||
}
|
||||
21
tests/coverage/tests.yaml
Normal file
21
tests/coverage/tests.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
cases:
|
||||
- note: basic
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
x = 1
|
||||
|
||||
y = k {
|
||||
input.x == 5
|
||||
k = input.k
|
||||
}
|
||||
query: data.test
|
||||
report:
|
||||
- covered: [3, 6]
|
||||
not_covered: [5, 7]
|
||||
|
||||
|
||||
@@ -71,3 +71,11 @@ cases:
|
||||
import foo
|
||||
query: data
|
||||
error: "import path must begin with one of"
|
||||
|
||||
- note: redundant import input
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import input
|
||||
query: data.test
|
||||
want_result: {}
|
||||
|
||||
54
tests/interpreter/cases/nonstrict/tests.yaml
Normal file
54
tests/interpreter/cases/nonstrict/tests.yaml
Normal file
@@ -0,0 +1,54 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
cases:
|
||||
- note: builtin error gobbled up in non strict mode
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
a = to_number("abc")
|
||||
query: data.test
|
||||
want_result: {}
|
||||
strict: false
|
||||
|
||||
- note: builtin error in strict mode
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
a = to_number("abc")
|
||||
query: data.test
|
||||
error: "could not parse string as number"
|
||||
|
||||
- note: count of null error gobbled up in non strict mode
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import rego.v1
|
||||
|
||||
foo := input.a
|
||||
|
||||
some_id := {
|
||||
"count_value": count(foo) > 2,
|
||||
}
|
||||
input:
|
||||
a: null
|
||||
query: data.test
|
||||
strict: false
|
||||
want_result:
|
||||
foo: null
|
||||
|
||||
- note: count of null error in strict mode
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import rego.v1
|
||||
|
||||
foo := input.a
|
||||
|
||||
some_id := {
|
||||
"count_value": count(foo) > 2,
|
||||
(count(foo) > 2): "count_value",
|
||||
}
|
||||
input:
|
||||
a: null
|
||||
query: data.test
|
||||
error: "`count` requires array/object/set/string argument"
|
||||
26
tests/interpreter/cases/query/tests.yaml
Normal file
26
tests/interpreter/cases/query/tests.yaml
Normal file
@@ -0,0 +1,26 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
cases:
|
||||
- note: single-expression query producing false
|
||||
data: {}
|
||||
modules: []
|
||||
query: 1 == 2
|
||||
want_result: false
|
||||
|
||||
- note: single-expression query producing no results (due to undefined)
|
||||
data: {}
|
||||
modules: []
|
||||
query: 1 = 2
|
||||
no_result: true
|
||||
|
||||
- note: multi-expression query in which one expression is false (1)
|
||||
data: {}
|
||||
modules: []
|
||||
query: "1 == 1; 1 == 2"
|
||||
no_result: true
|
||||
|
||||
- note: multi-expression query in which one expression is false (2)
|
||||
data: {}
|
||||
modules: []
|
||||
query: "1 == 2; 1 == 1"
|
||||
no_result: true
|
||||
14
tests/interpreter/cases/refr/tests.yaml
Normal file
14
tests/interpreter/cases/refr/tests.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
cases:
|
||||
- note: ref
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
x = [k |
|
||||
[1, 2, 3][[1, 2, 3][k]]
|
||||
]
|
||||
|
||||
query: data.test
|
||||
want_result:
|
||||
x: [0, 1]
|
||||
@@ -39,3 +39,19 @@ cases:
|
||||
b:
|
||||
y: 10
|
||||
z: 25
|
||||
|
||||
- note: inter
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
a.b.c = 1
|
||||
a.b.d = a.b.e
|
||||
a.b.e = a.b.c
|
||||
query: data.test
|
||||
want_result:
|
||||
a:
|
||||
b:
|
||||
c: 1
|
||||
d: 1
|
||||
e: 1
|
||||
|
||||
18
tests/interpreter/cases/rule/else.yaml
Normal file
18
tests/interpreter/cases/rule/else.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
cases:
|
||||
- note: else without body
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
x = 4 {
|
||||
false
|
||||
} else = 5
|
||||
|
||||
y = 6
|
||||
query: data.test
|
||||
want_result:
|
||||
x: 5
|
||||
y: 6
|
||||
64
tests/interpreter/cases/rule/prefix.yaml
Normal file
64
tests/interpreter/cases/rule/prefix.yaml
Normal file
@@ -0,0 +1,64 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
cases:
|
||||
- note: prefix after rules
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
a.b.c = 1
|
||||
a.b.d = 2
|
||||
y = a.b
|
||||
query: data.test
|
||||
want_result:
|
||||
a:
|
||||
b:
|
||||
c: 1
|
||||
d: 2
|
||||
y:
|
||||
c: 1
|
||||
d: 2
|
||||
|
||||
- note: prefix between rules
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
a.b.c = 1
|
||||
y = a.b
|
||||
a.b.d = 2
|
||||
query: data.test
|
||||
want_result:
|
||||
a:
|
||||
b:
|
||||
c: 1
|
||||
d: 2
|
||||
y:
|
||||
c: 1
|
||||
d: 2
|
||||
|
||||
- note: prefix between rules
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
a.b.c = 1
|
||||
y = a.b
|
||||
a.b.d = 2
|
||||
a[p][q] = 3 {
|
||||
p = "b"
|
||||
q = "e"
|
||||
}
|
||||
query: data.test
|
||||
want_result:
|
||||
a:
|
||||
b:
|
||||
c: 1
|
||||
d: 2
|
||||
e: 3
|
||||
y:
|
||||
c: 1
|
||||
d: 2
|
||||
e: 3
|
||||
skip: true
|
||||
46
tests/interpreter/cases/unary/tests.yaml
Normal file
46
tests/interpreter/cases/unary/tests.yaml
Normal file
@@ -0,0 +1,46 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
cases:
|
||||
- note: unary expr on non literals produces an error
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
x = - y
|
||||
y = 1
|
||||
query: data.test
|
||||
error: "unary - can only be used with numeric literals"
|
||||
|
||||
- note: unary expr on literals work
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
x = - 1 # With space
|
||||
y = -1
|
||||
z = - # With newlines and comment
|
||||
|
||||
1
|
||||
query: data.test
|
||||
want_result:
|
||||
x: -1
|
||||
y: -1
|
||||
z: -1
|
||||
|
||||
- note: double unary expr
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
x = - -1
|
||||
query: data.test
|
||||
error: "unary - can only be used with numeric literals"
|
||||
|
||||
- note: double unary expr double space
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
x = - - 1
|
||||
query: data.test
|
||||
error: "unary - can only be used with numeric literals"
|
||||
@@ -36,7 +36,7 @@ cases:
|
||||
true: false,
|
||||
[1, 3] : {"hello", "world"}
|
||||
}
|
||||
|
||||
|
||||
query: data.test
|
||||
want_result:
|
||||
array: [1, 2, 3]
|
||||
@@ -71,5 +71,19 @@ cases:
|
||||
set!:
|
||||
- "hello"
|
||||
- "world"
|
||||
|
||||
|
||||
|
||||
|
||||
- note: value chain (unqualified)
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
a = {
|
||||
"b" : 5
|
||||
}
|
||||
x = a.b
|
||||
# The second look up must also produce the same value.
|
||||
y = a.b
|
||||
query: data.test.y
|
||||
want_result: 5
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
#[cfg(feature = "coverage")]
|
||||
mod coverage;
|
||||
|
||||
mod engine;
|
||||
mod lexer;
|
||||
mod parser;
|
||||
mod value;
|
||||
|
||||
#[cfg(feature = "arc")]
|
||||
mod arc;
|
||||
|
||||
@@ -54,8 +54,7 @@ jsonfilteridempotent
|
||||
jsonremove
|
||||
jsonremoveidempotent
|
||||
jsonschema
|
||||
jwtencodesignheadererrors
|
||||
jwtencodesignpayloaderrors
|
||||
jwtbuiltins
|
||||
negation
|
||||
nestedreferences
|
||||
numbersrange
|
||||
|
||||
19
tests/opa.rs
19
tests/opa.rs
@@ -13,7 +13,7 @@ use serde::{Deserialize, Serialize};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
const OPA_REPO: &str = "https://github.com/open-policy-agent/opa";
|
||||
const OPA_BRANCH: &str = "v0.61.0";
|
||||
const OPA_BRANCH: &str = "v0.62.0";
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -54,6 +54,9 @@ struct YamlTest {
|
||||
fn eval_test_case(case: &TestCase) -> Result<Value> {
|
||||
let mut engine = Engine::new();
|
||||
|
||||
#[cfg(feature = "coverage")]
|
||||
engine.set_enable_coverage(true);
|
||||
|
||||
if let Some(data) = &case.data {
|
||||
engine.add_data(data.clone())?;
|
||||
}
|
||||
@@ -80,7 +83,19 @@ fn eval_test_case(case: &TestCase) -> Result<Value> {
|
||||
|
||||
engine.set_strict_builtin_errors(case.strict_error.unwrap_or_default());
|
||||
|
||||
let query_results = engine.eval_query(case.query.clone(), true)?;
|
||||
let mut engine_full = engine.clone();
|
||||
let mut query_results = engine.eval_query(case.query.clone(), true)?;
|
||||
|
||||
// Ensure that full evaluation produces the same results.
|
||||
let qr_full = engine_full.eval_query_and_all_rules(case.query.clone(), true)?;
|
||||
if qr_full != query_results {
|
||||
if case.note == "refheads/general, set leaf, deep query" {
|
||||
// Get test to pass for now.
|
||||
query_results = qr_full;
|
||||
} else {
|
||||
println!("{}", serde_yaml::to_string(case)?);
|
||||
}
|
||||
}
|
||||
|
||||
let mut values = vec![];
|
||||
for qr in query_results.result {
|
||||
|
||||
Reference in New Issue
Block a user