Compare commits

...

9 Commits

Author SHA1 Message Date
Anand Krishnamoorthi
1ab27b253b chore: release (#121)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-02-01 16:16:33 -08:00
Anand Krishnamoorthi
761d11ef48 Document bindings (#119)
* Instructions for WASM/JS binding

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* Document Python, WASM/JS bindings

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

---------

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2024-02-01 13:47:27 -08:00
Anand Krishnamoorthi
beea2274d3 Conform to OPA 0.61.0. (#118)
Implement `import rego.v1`
https://www.openpolicyagent.org/docs/latest/policy-language/#the-regov1-import

- `if` required before rule body
- import rego.v1 automatically imports future.keywords
- handle import shadowing
- data, input cannot be shadowed
- deprecated functions as disallowed
- rules must have assignment or body
- `contains` required for parital set

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2024-01-31 21:39:02 -08:00
Anand Krishnamoorthi
5799a3e6c4 Update publish-python.yml 2024-01-28 23:55:15 -08:00
Anand Krishnamoorthi
ca91c0ea20 Publish python packages (#117)
Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2024-01-28 23:12:06 -08:00
Anand Krishnamoorthi
bf75813c43 Publish wasm (#116)
* Create jekyll-gh-pages.yml

* Fix publish-wasm

- --release instead of -r
- set working directory
- use v4

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

---------

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2024-01-28 16:45:52 -08:00
Anand Krishnamoorthi
35ec9c03ad Set working-directory for wasm-pack 2024-01-28 15:30:41 -08:00
Anand Krishnamoorthi
8ca863c661 Python bindings (#115)
Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2024-01-28 14:39:59 -08:00
Anand Krishnamoorthi
055bdd295f WASM binding (#114)
Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2024-01-27 22:14:29 -08:00
29 changed files with 1701 additions and 30 deletions

114
.github/workflows/publish-python.yml vendored Normal file
View File

@@ -0,0 +1,114 @@
# This file is autogenerated by maturin v1.4.0
# To update, run
#
# maturin generate-ci --manifest-path bindings/python/Cargo.toml github
#
name: publish-python
on:
workflow_dispatch:
permissions:
contents: read
jobs:
linux:
runs-on: ubuntu-latest
strategy:
matrix:
target: [x86_64, x86, aarch64, armv7, s390x, ppc64le]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
args: --release --out dist --find-interpreter --manifest-path bindings/python/Cargo.toml
sccache: 'true'
manylinux: auto
- name: Upload wheels
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
windows:
runs-on: windows-latest
strategy:
matrix:
target: [x64, x86]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.10'
architecture: ${{ matrix.target }}
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
args: --release --out dist --find-interpreter --manifest-path bindings/python/Cargo.toml
sccache: 'true'
- name: Upload wheels
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
macos:
runs-on: macos-latest
strategy:
matrix:
target: [x86_64, aarch64, universal2-apple-darwin]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
args: --release --out dist --find-interpreter --manifest-path bindings/python/Cargo.toml
sccache: 'true'
- name: Upload wheels
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
sdist:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build sdist
uses: PyO3/maturin-action@v1
with:
command: sdist
args: --out dist --manifest-path bindings/python/Cargo.toml
- name: Upload sdist
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
release:
name: Release
runs-on: ubuntu-latest
# Commented out for initial release.
# if: "startsWith(github.ref, 'refs/tags/')"
needs: [linux, windows, macos, sdist]
steps:
- uses: actions/download-artifact@v3
with:
name: wheels
- name: Publish to PyPI
uses: PyO3/maturin-action@v1
env:
MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
with:
command: upload
args: --non-interactive --skip-existing *

32
.github/workflows/publish-wasm.yml vendored Normal file
View File

@@ -0,0 +1,32 @@
name: publish-wasm
permissions:
pull-requests: write
contents: write
on: workflow_dispatch
jobs:
publish-wasm:
name: publish
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
# Setup .npmrc file to publish to npm
- uses: actions/setup-node@v4
with:
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
- name: Install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- name: Build
run: wasm-pack build --target nodejs --release
working-directory: ./bindings/wasm
- name: Publish
run: wasm-pack publish --target nodejs
working-directory: ./bindings/wasm
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}

View File

@@ -37,3 +37,11 @@ jobs:
- name: Run tests (OPA Conformance)
run: >-
cargo test -r --test opa -- $(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

View File

@@ -6,6 +6,129 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [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
- fix bitwise.and and add tests ([#19](https://github.com/microsoft/regorus/pull/19))
### Other
- Document bindings ([#119](https://github.com/microsoft/regorus/pull/119))
- Conform to OPA 0.61.0. ([#118](https://github.com/microsoft/regorus/pull/118))
- Update publish-python.yml
- Publish python packages ([#117](https://github.com/microsoft/regorus/pull/117))
- Publish wasm ([#116](https://github.com/microsoft/regorus/pull/116))
- Set working-directory for wasm-pack
- Python bindings ([#115](https://github.com/microsoft/regorus/pull/115))
- WASM binding ([#114](https://github.com/microsoft/regorus/pull/114))
- release ([#112](https://github.com/microsoft/regorus/pull/112))
- Improve crate documentation ([#111](https://github.com/microsoft/regorus/pull/111))
- Try out manual trigger for release-plz ([#110](https://github.com/microsoft/regorus/pull/110))
- - Document Location, Expression, QueryResult ([#109](https://github.com/microsoft/regorus/pull/109))
- Update Cargo.toml ([#108](https://github.com/microsoft/regorus/pull/108))
- Change version to `0.1.0-alpha.1` ([#107](https://github.com/microsoft/regorus/pull/107))
- Add crate documentation ([#106](https://github.com/microsoft/regorus/pull/106))
- Release preparation ([#105](https://github.com/microsoft/regorus/pull/105))
- Update READEME.md with current status, grammar etc. ([#102](https://github.com/microsoft/regorus/pull/102))
- Implement builtin `time.parse_duration_ns` method ([#100](https://github.com/microsoft/regorus/pull/100))
- Implement import keyword ([#101](https://github.com/microsoft/regorus/pull/101))
- OPA conformance: Pass refheads test suite ([#90](https://github.com/microsoft/regorus/pull/90))
- OPA conformance: Ensure that `withkeyword` OPA tests pass ([#88](https://github.com/microsoft/regorus/pull/88))
- Handle walk builtin as a loop expression ([#86](https://github.com/microsoft/regorus/pull/86))
- Implement most of the builtin `time` module ([#82](https://github.com/microsoft/regorus/pull/82))
- OPA Conformance
- OPA conformance ([#81](https://github.com/microsoft/regorus/pull/81))
- More OPA conformance ([#77](https://github.com/microsoft/regorus/pull/77))
- OPA conformance ([#71](https://github.com/microsoft/regorus/pull/71))
- Builtin UUID module ([#68](https://github.com/microsoft/regorus/pull/68))
- Add tests for builtin `string::format_int` method ([#65](https://github.com/microsoft/regorus/pull/65))
- More builtins and semantic improvements ([#66](https://github.com/microsoft/regorus/pull/66))
- More OPA conformance; in-progress: ability to trace interpreter ([#63](https://github.com/microsoft/regorus/pull/63))
- More OPA conformant semantics ([#62](https://github.com/microsoft/regorus/pull/62))
- Updated readme. Added bundle support. ([#61](https://github.com/microsoft/regorus/pull/61))
- crypto builtins ([#57](https://github.com/microsoft/regorus/pull/57))
- Regex and Glob builtins ([#56](https://github.com/microsoft/regorus/pull/56))
- Formalize concept of a Number ([#55](https://github.com/microsoft/regorus/pull/55))
- Lock down ACI tests and more OPA test folders ([#54](https://github.com/microsoft/regorus/pull/54))
- Fix scheduling regression ([#53](https://github.com/microsoft/regorus/pull/53))
- add full api to engine ([#50](https://github.com/microsoft/regorus/pull/50))
- Use Rc<str> instead of string. ([#52](https://github.com/microsoft/regorus/pull/52))
- More library functions ([#51](https://github.com/microsoft/regorus/pull/51))
- Added semver.is_valid and semver.compare ([#49](https://github.com/microsoft/regorus/pull/49))
- OPA conformance tests ([#45](https://github.com/microsoft/regorus/pull/45))
- Avoid dependency on `source lifetime. ([#43](https://github.com/microsoft/regorus/pull/43))
- Allow with modifier for builtin and user functions ([#42](https://github.com/microsoft/regorus/pull/42))
- Special cases of refs to data ([#41](https://github.com/microsoft/regorus/pull/41))
- Fix scheduling statements that don't create bindings ([#40](https://github.com/microsoft/regorus/pull/40))
- Ability to run the OPA testsuite ([#39](https://github.com/microsoft/regorus/pull/39))
- Engine ([#38](https://github.com/microsoft/regorus/pull/38))
- Use Ref for storing ast nodes in collections. ([#37](https://github.com/microsoft/regorus/pull/37))
- all, any deprecated functions ([#35](https://github.com/microsoft/regorus/pull/35))
- all, any deprecated functions ([#34](https://github.com/microsoft/regorus/pull/34))
- Improvements ([#33](https://github.com/microsoft/regorus/pull/33))
- Order query expression results ([#32](https://github.com/microsoft/regorus/pull/32))
- Scheduling of statements in user queries ([#31](https://github.com/microsoft/regorus/pull/31))
- eval, lex, parse commands ([#30](https://github.com/microsoft/regorus/pull/30))
- eval_user_query for OPA style results ([#29](https://github.com/microsoft/regorus/pull/29))
- Arity for builtins ([#28](https://github.com/microsoft/regorus/pull/28))
- Handle chained _ ([#27](https://github.com/microsoft/regorus/pull/27))
- Minimize PR 22 ([#26](https://github.com/microsoft/regorus/pull/26))
- improve errors location ([#23](https://github.com/microsoft/regorus/pull/23))
- Fix clippy warning ([#25](https://github.com/microsoft/regorus/pull/25))
- negation of an undefined value should return true ([#21](https://github.com/microsoft/regorus/pull/21))
- Ensure that scopes are cleaned up correctly upon error. ([#20](https://github.com/microsoft/regorus/pull/20))
- support of or-functions ([#18](https://github.com/microsoft/regorus/pull/18))
- Statement Scheduler Implementation
- Remove unnecessary lifetime
- json.filter, object.filter, object.get, object.keys, object.remove
- :to_number builtin
- :trace builtin
- bitwise builtins
- :print builtin
- Partial sprintf implementation.
- All string functions except sprintf. TODO: Add tests
- More string functions without tests
- More string functions
- concat and contains
- string concat (WIP)
- Support build on non Linux platforms
- Prepare for upstreaming
- Test for multi-assign
- Support dependencies between vars defined in same statement
- Statement scheduler (WIP)
- Print small-form table of files without 100% coverage.
- Code tweaks to improve coverage
- Tests for aggregates builtins
- Tests for numbers builtins
- Tests for arrays builtins
- Tests for types functions
- Destructuring of arrays and objects in some-in expressions
- `some .. in` implementation
- Fix key, value in membership and some-in
- refactor
- Arrays and Aggregates
- Implement `every` statement ([#4](https://github.com/microsoft/regorus/pull/4))
- Set loop index variable if not "_" ([#3](https://github.com/microsoft/regorus/pull/3))
- Allow comprehensions in default value. ([#2](https://github.com/microsoft/regorus/pull/2))
- Lock down numbers
- mod function
- Builtin functions for numbers (WIP)
- Implement comparison operators. Formalize semantics.
- Rework assign operations ([#6](https://github.com/microsoft/regorus/pull/6))
- Locked down supported values in default rule.
- Improvements to github workflow ([#4](https://github.com/microsoft/regorus/pull/4))
- Update name to regorus
- Update rust.yml
- Add simple git action
- Add missing config.toml
- Update license to MIT
- Code from github.com/anakrish/rego-rs
- SUPPORT.md committed
- SECURITY.md committed
- README.md committed
- LICENSE committed
- CODE_OF_CONDUCT.md committed
- Initial commit
## [0.1.0-alpha.2](https://github.com/microsoft/regorus/compare/v0.1.0-alpha.1...v0.1.0-alpha.2) - 2024-01-19
### Other

View File

@@ -1,7 +1,14 @@
[workspace]
members = [
"bindings/python",
"bindings/wasm"
]
[package]
name = "regorus"
description = "A fast, lightweight Rego (OPA policy language) interpreter"
version = "0.1.0-alpha.2"
version = "0.1.0-alpha.3"
edition = "2021"
license-file = "LICENSE"
repository = "https://github.com/microsoft/regorus"
@@ -82,7 +89,6 @@ 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 }
document-features = "0.2.8"
[dev-dependencies]

View File

@@ -58,9 +58,16 @@ $ cargo build -r --example regorus --features "yaml" --no-default-features; stri
```
Regorus passes the [OPA v0.60.0 test-suite](https://www.openpolicyagent.org/docs/latest/ir/#test-suite) barring a few
Regorus passes the [OPA v0.61.0 test-suite](https://www.openpolicyagent.org/docs/latest/ir/#test-suite) barring a few
builtins. See [OPA Conformance](#opa-conformance) below.
## Bindings
Regorus can be used from a variety of languages:
- Javascript: To compile Regorus to WASM and use it in Javascript, see [bindings/wasm](bindings/wasm)
- Python: To use Regorus from Python, see [bindings/python](bindings/python)
## Getting Started
[examples/regorus](https://github.com/microsoft/regorus/blob/main/examples/regorus.rs) is an example program that
@@ -192,7 +199,7 @@ Benchmark 1: opa eval -b tests/aci -d tests/aci/data.json -i tests/aci/input.jso
```
## OPA Conformance
Regorus has been verified to be compliant with [OPA v0.60.0](https://github.com/open-policy-agent/opa/releases/tag/v0.60.0)
Regorus has been verified to be compliant with [OPA v0.61.0](https://github.com/open-policy-agent/opa/releases/tag/v0.61.0)
using a [test driver](https://github.com/microsoft/regorus/blob/main/tests/opa.rs) that loads and runs the OPA testsuite using Regorus, and verifies that expected outputs
are produced.

View File

@@ -0,0 +1 @@
pyo3

View File

@@ -0,0 +1,14 @@
# 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/regoruspy-v0.1.0) - 2024-02-01
### Other
- Document bindings ([#119](https://github.com/microsoft/regorus/pull/119))
- Publish python packages ([#117](https://github.com/microsoft/regorus/pull/117))
- Python bindings ([#115](https://github.com/microsoft/regorus/pull/115))

View File

@@ -0,0 +1,20 @@
[package]
name = "regoruspy"
version = "0.1.0"
edition = "2021"
repository = "https://github.com/microsoft/regorus/bindings/python"
description = "Python 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"
ordered-float = "4.2.0"
pyo3 = {version = "0.20.2", features = ["anyhow", "extension-module"] }
regorus = { path = "../.." }
serde_json = "1.0.112"

67
bindings/python/README.md Normal file
View File

@@ -0,0 +1,67 @@
# regorus
**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.
Regorus can be used in Python via `regorus` package. (It is not yet available in PyPI, but can be manually built.)
See [Repository](https://github.com/microsoft/regorus).
To build this binding, see [building](https://github.com/microsoft/regorus/bindings/python/building.md)
## Usage
```Python
import regorus
# Create engine
engine = regorus.Engine()
# Load policies
engine.add_policy_from_file('../../tests/aci/framework.rego')
engine.add_policy_from_file('../../tests/aci/api.rego')
engine.add_policy_from_file('../../tests/aci/policy.rego')
# Add policy data
data = {
"metadata": {
"devices": {
"/run/layers/p0-layer0": "1b80f120dbd88e4355d6241b519c3e25290215c469516b49dece9cf07175a766",
"/run/layers/p0-layer1": "e769d7487cc314d3ee748a4440805317c19262c7acd2fdbdb0d47d2e4613a15c",
"/run/layers/p0-layer2": "eb36921e1f82af46dfe248ef8f1b3afb6a5230a64181d960d10237a08cd73c79",
"/run/layers/p0-layer3": "41d64cdeb347bf236b4c13b7403b633ff11f1cf94dbc7cf881a44d6da88c5156",
"/run/layers/p0-layer4": "4dedae42847c704da891a28c25d32201a1ae440bce2aecccfa8e6f03b97a6a6c",
"/run/layers/p0-layer5": "fe84c9d5bfddd07a2624d00333cf13c1a9c941f3a261f13ead44fc6a93bc0e7a"
}
}
}
engine.add_data(data)
# Set input
input = {
"containerID": "container0",
"layerPaths": [
"/run/layers/p0-layer0",
"/run/layers/p0-layer1",
"/run/layers/p0-layer2",
"/run/layers/p0-layer3",
"/run/layers/p0-layer4",
"/run/layers/p0-layer5"
],
"target": "/run/gcs/c/container0/rootfs"
}
engine.set_input(input)
# Eval query
results = engine.eval_query('data.framework.mount_overlay=x')
# Print results
print(results['result'][0])
# Eval query as json
results_json = engine.eval_query_as_json('data.framework.mount_overlay=x')
print(results_json)
```

View File

@@ -0,0 +1,22 @@
- Install maturin
```
pipx install maturin
```
See [Maturin User Guide](https://www.maturin.rs)
- Build bindings for Python
```
cd bindings/python
maturin build --release --target-dir wheels
```
- Install python wheel
```
pip3 install ../../target/wheels/regorus*.whl --force-reinstall
```
- Run test script
```
python3 test.py
```

View File

@@ -0,0 +1,16 @@
[build-system]
requires = ["maturin>=1.4,<2.0"]
build-backend = "maturin"
[project]
name = "regorus"
requires-python = ">=3.8"
classifiers = [
"Programming Language :: Rust",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
]
dynamic = ["version"]
[tool.maturin]
features = ["pyo3/extension-module"]

301
bindings/python/src/lib.rs Normal file
View File

@@ -0,0 +1,301 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use anyhow::{anyhow, Result};
use pyo3::exceptions::PyTypeError;
use pyo3::prelude::*;
use pyo3::types::*;
use std::collections::{BTreeMap, BTreeSet};
use ::regorus::Value;
/// Regorus engine.
#[pyclass(unsendable)]
pub struct Engine {
engine: ::regorus::Engine,
}
impl Default for Engine {
fn default() -> Self {
Self::new()
}
}
impl Clone for Engine {
/// Clone a [`Engine`]
///
/// To avoid having to parse same policy again, the engine can be cloned
/// after policies and data have been added.
fn clone(&self) -> Self {
Self {
engine: self.engine.clone(),
}
}
}
fn from<'source>(ob: &'source PyAny) -> Result<Value, PyErr> {
// dicts
Ok(if let Ok(dict) = ob.downcast::<PyDict>() {
let mut map = BTreeMap::new();
for (k, v) in dict {
map.insert(from(k)?, from(v)?);
}
map.into()
}
// set
else if let Ok(pset) = ob.downcast::<PySet>() {
let mut set = BTreeSet::new();
for v in pset {
set.insert(from(v)?);
}
set.into()
}
// frozen set
else if let Ok(pfset) = ob.downcast::<PyFrozenSet>() {
//
let mut set = BTreeSet::new();
for v in pfset {
set.insert(from(v)?);
}
set.into()
}
// lists and tuples
else if let Ok(plist) = ob.downcast::<PyList>() {
let mut array = Vec::new();
for v in plist {
array.push(from(v)?);
}
array.into()
} else if let Ok(ptuple) = ob.downcast::<PyTuple>() {
let mut array = Vec::new();
for v in ptuple {
array.push(from(v)?);
}
array.into()
}
// String
else if let Ok(s) = String::extract(ob) {
s.into()
}
// Numeric
else if let Ok(v) = i64::extract(ob) {
v.into()
} else if let Ok(v) = u64::extract(ob) {
v.into()
} else if let Ok(v) = f64::extract(ob) {
v.into()
}
// Boolean
else if let Ok(b) = bool::extract(ob) {
b.into()
}
// None
else if ob.downcast::<PyNone>().is_ok() {
Value::Null
}
// Anything that is a sequence
else if let Ok(pseq) = ob.downcast::<PySequence>() {
let mut array = Vec::new();
for i in 0..pseq.len()? {
array.push(from(pseq.get_item(i)?)?);
}
array.into()
}
// Anything that is a map
else if let Ok(pmap) = ob.downcast::<PyMapping>() {
let mut map = BTreeMap::new();
let keys = pmap.keys()?;
let values = pmap.values()?;
for i in 0..keys.len()? {
let key = keys.get_item(i)?;
let value = values.get_item(i)?;
map.insert(from(key)?, from(value)?);
}
map.into()
} else {
return Err(PyErr::new::<PyTypeError, _>(
"object cannot be converted to RegoValue",
));
})
}
fn to(mut v: Value, py: Python<'_>) -> Result<PyObject> {
Ok(match v {
Value::Null => None::<u64>.to_object(py),
// TODO: Revisit this mapping
Value::Undefined => None::<u64>.to_object(py),
Value::Bool(b) => b.to_object(py),
Value::String(s) => s.to_object(py),
Value::Number(_) => {
if let Ok(f) = v.as_f64() {
f.to_object(py)
} else if let Ok(u) = v.as_u64() {
u.to_object(py)
} else {
v.as_i64()?.to_object(py)
}
}
Value::Array(_) => {
let list = PyList::empty(py);
for v in std::mem::replace(v.as_array_mut()?, Vec::new()) {
list.append(to(v, py)?)?;
}
list.into()
}
Value::Set(_) => {
let set = PySet::empty(py)?;
for v in std::mem::replace(v.as_set_mut()?, BTreeSet::new()) {
set.add(to(v, py)?)?;
}
set.into()
}
Value::Object(_) => {
let dict = PyDict::new(py);
for (k, v) in std::mem::replace(v.as_object_mut()?, BTreeMap::new()) {
dict.set_item(to(k, py)?, to(v, py)?)?;
}
dict.into()
}
})
}
#[pymethods]
impl Engine {
/// Construct a new Engine
#[new]
pub fn new() -> Self {
Self {
engine: ::regorus::Engine::new(),
}
}
/// Add a policy
///
/// The policy is parsed into AST.
///
/// * `path`: A filename to be associated with the policy.
/// * `rego`: Rego policy.
pub fn add_policy(&mut self, path: String, rego: String) -> Result<()> {
self.engine.add_policy(path, rego)
}
/// Add a policy from given file.
///
/// The policy is parsed into AST.
///
/// * `path`: Path to the policy file.
pub fn add_policy_from_file(&mut self, path: String) -> Result<()> {
self.engine.add_policy_from_file(path)
}
/// Add policy data.
///
/// * `data`: Rego value. A Rego value is a number, bool, string, None
/// or a list/set/map whose items themselves are Rego values.
pub fn add_data(&mut self, data: &PyAny) -> Result<()> {
let data = from(data)?;
self.engine.add_data(data)
}
/// Add policy data.
///
/// * `data`: JSON encoded value to be used as policy data.
pub fn add_data_json(&mut self, data: String) -> Result<()> {
let data = Value::from_json_str(&data)?;
self.engine.add_data(data)
}
/// Add policy data from file.
///
/// * `path`: Path to JSON policy data.
pub fn add_data_from_json_file(&mut self, path: String) -> Result<()> {
let data = Value::from_json_file(&path)?;
self.engine.add_data(data)
}
/// Clear policy data.
pub fn clear_data(&mut self) -> Result<()> {
self.engine.clear_data();
Ok(())
}
/// Set input.
///
/// * `input`: Rego value. A Rego value is a number, bool, string, None
/// or a list/set/map whose items themselves are Rego values.
pub fn set_input(&mut self, input: &PyAny) -> Result<()> {
let input = from(input)?;
self.engine.set_input(input);
Ok(())
}
/// Set input.
///
/// * `input`: JSON encoded value to be used as input to query.
pub fn set_input_json(&mut self, input: String) -> Result<()> {
let input = Value::from_json_str(&input)?;
self.engine.set_input(input);
Ok(())
}
/// Set input.
///
/// * `path`: Path to JSON input data.
pub fn set_input_from_json_file(&mut self, path: String) -> Result<()> {
let input = Value::from_json_file(&path)?;
self.engine.set_input(input);
Ok(())
}
/// Evaluate query.
///
/// * `query`: Rego expression to be evaluate.
pub fn eval_query(&mut self, query: String, py: Python<'_>) -> Result<PyObject> {
let results = self.engine.eval_query(query, false)?;
let rlist = PyList::empty(py);
for result in results.result.into_iter() {
let rdict = PyDict::new(py);
let elist = PyList::empty(py);
for expr in result.expressions.into_iter() {
let edict = PyDict::new(py);
edict.set_item("value".to_object(py), to(expr.value, py)?)?;
edict.set_item("text".to_object(py), expr.text.as_ref().to_object(py))?;
let ldict = PyDict::new(py);
ldict.set_item("row".to_object(py), expr.location.row.to_object(py))?;
ldict.set_item("col".to_object(py), expr.location.col.to_object(py))?;
edict.set_item("location".to_object(py), ldict)?;
elist.append(edict)?;
}
rdict.set_item("expressions".to_object(py), elist)?;
rdict.set_item("bindings".to_object(py), to(result.bindings, py)?)?;
rlist.append(rdict)?;
}
let dict = PyDict::new(py);
dict.set_item("result".to_object(py), rlist)?;
Ok(dict.into())
}
/// Evaluate query. Returns result as JSON.
///
/// * `query`: Rego expression to be evaluate.
pub fn eval_query_as_json(&mut self, query: String) -> Result<String> {
let results = self.engine.eval_query(query, false)?;
serde_json::to_string_pretty(&results).map_err(|e| anyhow!("{e}"))
}
}
#[pymodule]
pub fn regorus(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
m.add_class::<crate::Engine>()
}

52
bindings/python/test.py Normal file
View File

@@ -0,0 +1,52 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
import regorus
# Create engine
engine = regorus.Engine()
# Load policies
engine.add_policy_from_file('../../tests/aci/framework.rego')
engine.add_policy_from_file('../../tests/aci/api.rego')
engine.add_policy_from_file('../../tests/aci/policy.rego')
# Add policy data
data = {
"metadata": {
"devices": {
"/run/layers/p0-layer0": "1b80f120dbd88e4355d6241b519c3e25290215c469516b49dece9cf07175a766",
"/run/layers/p0-layer1": "e769d7487cc314d3ee748a4440805317c19262c7acd2fdbdb0d47d2e4613a15c",
"/run/layers/p0-layer2": "eb36921e1f82af46dfe248ef8f1b3afb6a5230a64181d960d10237a08cd73c79",
"/run/layers/p0-layer3": "41d64cdeb347bf236b4c13b7403b633ff11f1cf94dbc7cf881a44d6da88c5156",
"/run/layers/p0-layer4": "4dedae42847c704da891a28c25d32201a1ae440bce2aecccfa8e6f03b97a6a6c",
"/run/layers/p0-layer5": "fe84c9d5bfddd07a2624d00333cf13c1a9c941f3a261f13ead44fc6a93bc0e7a"
}
}
}
engine.add_data(data)
# Set input
input = {
"containerID": "container0",
"layerPaths": [
"/run/layers/p0-layer0",
"/run/layers/p0-layer1",
"/run/layers/p0-layer2",
"/run/layers/p0-layer3",
"/run/layers/p0-layer4",
"/run/layers/p0-layer5"
],
"target": "/run/gcs/c/container0/rootfs"
}
engine.set_input(input)
# Eval query
results = engine.eval_query('data.framework.mount_overlay=x')
# Print results
print(results['result'][0])
# Eval query as json
results_json = engine.eval_query_as_json('data.framework.mount_overlay=x')
print(results_json)

View File

@@ -0,0 +1,14 @@
# 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/regorusjs-v0.1.0) - 2024-02-01
### Other
- Document bindings ([#119](https://github.com/microsoft/regorus/pull/119))
- Python bindings ([#115](https://github.com/microsoft/regorus/pull/115))
- WASM binding ([#114](https://github.com/microsoft/regorus/pull/114))

19
bindings/wasm/Cargo.toml Normal file
View File

@@ -0,0 +1,19 @@
[package]
name = "regorusjs"
version = "0.1.0"
edition = "2021"
repository = "https://github.com/microsoft/regorus/bindings/wasm"
description = "WASM 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]
regorus = { path = "../.." }
serde_json = "1.0.111"
wasm-bindgen = "0.2.90"
[dev-dependencies]
wasm-bindgen-test = "0.3.40"

81
bindings/wasm/README.md Normal file
View File

@@ -0,0 +1,81 @@
# regorusjs
**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.
`regorusjs` is Regorus compiled into WASM.
See [Repository](https://github.com/microsoft/regorus).
To build this binding, see [building](https://github.com/microsoft/regorus/bindings/wasm/building.md)
## Usage
```javascript
var regorus = require('regorusjs')
// Create an engine.
var engine = new regorus.Engine();
// Add Rego policy.
engine.add_policy(
// Associate this file name with policy
'hello.rego',
// Rego policy
`
package test
# Join messages
message = concat(", ", [input.message, data.message])
`)
// Set policy data
engine.add_data_json(`
{
"message" : "World!"
}
`)
// Set policy input
engine.set_input_json(`
{
"message" : "Hello"
}
`)
// Eval query
results = engine.eval_query('data.test.message')
// Display
console.log(results)
// {
// "result": [
// {
// "expressions": [
// {
// "value": "Hello, World!",
// "text": "data.test.message",
// "location": {
// "row": 1,
// "col": 1
// }
// }
// ]
// }
// ]
// }
// Convert results to object
results = JSON.parse(results)
// Process result
console.log(results.result[0].expressions[0].value)
// Hello, World!
```

34
bindings/wasm/building.md Normal file
View File

@@ -0,0 +1,34 @@
- Install `wasm-pack`
```
cargo install wasm-pack
```
- Build `regorusjs` for nodejs.
```
cd bindings/wasm
wasm-pack build --target nodejs --release
```
- Install [nodejs](https://nodejs.org/en/download)
- Run the test script
```
$ node test.js
\\{
\\ "result": [
\\ {
\\ "expressions": [
\\ {
\\ "value": "Hello, World!",
\\ "text": "data.test.message",
\\ "location": {
\\ "row": 1,
\\ "col": 1
\\ }
\\ }
\\ ]
\\ }
\\ ]
\\}
```

144
bindings/wasm/src/lib.rs Normal file
View File

@@ -0,0 +1,144 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
/// WASM wrapper for [`regorus::Engine`]
pub struct Engine {
engine: regorus::Engine,
}
fn error_to_jsvalue<E: std::fmt::Display>(e: E) -> JsValue {
JsValue::from_str(&format!("{e}"))
}
impl Default for Engine {
fn default() -> Self {
Self::new()
}
}
impl Clone for Engine {
/// Clone a [`Engine`]
///
/// To avoid having to parse same policy again, the engine can be cloned
/// after policies and data have been added.
fn clone(&self) -> Self {
Self {
engine: self.engine.clone(),
}
}
}
#[wasm_bindgen]
impl Engine {
#[wasm_bindgen(constructor)]
/// Construct a new Engine
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html
pub fn new() -> Self {
Self {
engine: regorus::Engine::new(),
}
}
/// Add a policy
///
/// The policy is parsed into AST.
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.add_policy
///
/// * `path`: A filename to be associated with the policy.
/// * `rego`: Rego policy.
pub fn add_policy(&mut self, path: String, rego: String) -> Result<(), JsValue> {
self.engine.add_policy(path, rego).map_err(error_to_jsvalue)
}
/// Add policy data.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.add_data
/// * `data`: JSON encoded value to be used as policy data.
pub fn add_data_json(&mut self, data: String) -> Result<(), JsValue> {
let data = regorus::Value::from_json_str(&data).map_err(error_to_jsvalue)?;
self.engine.add_data(data).map_err(error_to_jsvalue)
}
/// Clear policy data.
pub fn clear_data(&mut self) -> Result<(), JsValue> {
self.engine.clear_data();
Ok(())
}
/// Set input.
///
/// See https://docs.rs/regorus/0.1.0-alpha.2/regorus/struct.Engine.html#method.set_input
/// * `input`: JSON encoded value to be used as input to query.
pub fn set_input_json(&mut self, input: String) -> Result<(), JsValue> {
let input = regorus::Value::from_json_str(&input).map_err(error_to_jsvalue)?;
self.engine.set_input(input);
Ok(())
}
/// Evaluate query.
///
/// See https://docs.rs/regorus/0.1.0-alpha.2/regorus/struct.Engine.html#method.eval_query
/// * `query`: Rego expression to be evaluate.
pub fn eval_query(&mut self, query: String) -> Result<String, JsValue> {
let results = self
.engine
.eval_query(query, false)
.map_err(error_to_jsvalue)?;
serde_json::to_string_pretty(&results).map_err(error_to_jsvalue)
}
}
#[cfg(test)]
mod tests {
use wasm_bindgen::prelude::*;
use wasm_bindgen_test::wasm_bindgen_test;
#[wasm_bindgen_test]
pub fn basic() -> Result<(), JsValue> {
let mut engine = crate::Engine::new();
// Exercise all APIs.
engine.add_data_json(
r#"
{
"foo" : "bar"
}
"#
.to_string(),
)?;
engine.set_input_json(
r#"
{
"message" : "Hello"
}
"#
.to_string(),
)?;
engine.add_policy(
"hello.rego".to_string(),
r#"
package test
message = input.message"#
.to_string(),
)?;
let results = engine.eval_query("data".to_string())?;
let r = regorus::Value::from_json_str(&results).map_err(crate::error_to_jsvalue)?;
let v = &r["result"][0]["expressions"][0]["value"];
// Ensure that input and policy were evaluated.
assert_eq!(v["test"]["message"], regorus::Value::from("Hello"));
// Test that data was set.
assert_eq!(v["foo"], regorus::Value::from("bar"));
Ok(())
}
}

47
bindings/wasm/test.js Normal file
View File

@@ -0,0 +1,47 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
var regorus = require('./pkg/regorusjs')
// Create an engine.
var engine = new regorus.Engine();
// Add Rego policy.
engine.add_policy(
// Associate this file name with policy
'hello.rego',
// Rego policy
`
package test
# Join messages
message = concat(", ", [input.message, data.message])
`)
// Set policy data
engine.add_data_json(`
{
"message" : "World!"
}
`)
// Set policy input
engine.set_input_json(`
{
"message" : "Hello"
}
`)
// Eval query
results = engine.eval_query('data.test.message')
// Display
console.log(results)
// Convert results to object
results = JSON.parse(results)
// Process result
console.log(results.result[0].expressions[0].value)

View File

@@ -353,6 +353,7 @@ pub struct Module {
pub package: Package,
pub imports: Vec<Import>,
pub policy: Vec<Ref<Rule>>,
pub rego_v1: bool,
}
pub type ExprRef = Ref<Expr>;

View File

@@ -13,7 +13,7 @@ use crate::QueryResults;
use std::convert::AsRef;
use std::path::Path;
use anyhow::Result;
use anyhow::{bail, Result};
/// The Rego evaluation engine.
///
@@ -175,6 +175,9 @@ impl Engine {
/// # }
/// ```
pub fn add_data(&mut self, data: Value) -> Result<()> {
if data.as_object().is_err() {
bail!("data must be object");
}
self.prepared = false;
self.interpreter.get_data_mut().merge(data)
}

View File

@@ -1998,14 +1998,14 @@ impl Interpreter {
}
}
fn lookup_function_by_name(&self, path: &str) -> Option<&Vec<Ref<Rule>>> {
fn lookup_function_by_name(&self, path: &str) -> Option<(&Vec<Ref<Rule>>, &Ref<Module>)> {
let mut path = path.to_owned();
if !path.starts_with("data.") {
path = self.current_module_path.clone() + "." + &path;
}
match self.functions.get(&path) {
Some((f, _)) => Some(f),
Some((f, _, m)) => Some((f, m)),
_ => None,
}
}
@@ -2058,7 +2058,8 @@ impl Interpreter {
#[cfg(feature = "deprecated")]
if let Some(builtin) = builtins::DEPRECATED.get(path) {
if !self.allow_deprecated {
let allow = self.allow_deprecated && !self.current_module()?.rego_v1;
if !allow {
bail!(span.error(format!("{path} is deprecated").as_str()))
}
return Ok(Some(builtin));
@@ -2120,9 +2121,9 @@ impl Interpreter {
_ => orig_fcn_path.clone(),
};
let empty = vec![];
let fcns_rules = match self.lookup_function_by_name(&fcn_path) {
Some(r) => r,
let empty: Vec<Ref<Rule>> = vec![];
let (fcns_rules, fcn_module) = match self.lookup_function_by_name(&fcn_path) {
Some((fcns, m)) => (fcns, Some(m.clone())),
_ => {
if self.default_rules.get(&fcn_path).is_some()
|| self
@@ -2131,10 +2132,10 @@ impl Interpreter {
.is_some()
{
// process default functions later.
&empty
(&empty, self.module.clone())
}
// Look up builtin function.
else if let Ok(Some(builtin)) = self.lookup_builtin(span, &fcn_path) {
else if let Some(builtin) = self.lookup_builtin(span, &fcn_path)? {
let r = self.eval_builtin_call(span, &fcn_path.clone(), *builtin, params);
if let Some(with_functions) = with_functions_saved {
self.with_functions = with_functions;
@@ -2213,6 +2214,7 @@ impl Interpreter {
..Context::default()
};
let prev_module = self.set_current_module(fcn_module.clone())?;
let value = match self.eval_rule_bodies(ctx, span, bodies) {
Ok(v) => v,
Err(e) => {
@@ -2222,6 +2224,7 @@ impl Interpreter {
continue;
}
};
self.set_current_module(prev_module)?;
let result = match &value {
Value::Set(s) if s.len() == 1 => s.iter().next().unwrap().clone(),

View File

@@ -15,6 +15,7 @@ pub struct Parser<'source> {
line: u16,
end: u16,
future_keywords: BTreeMap<String, Span>,
rego_v1: bool,
}
const FUTURE_KEYWORDS: [&str; 4] = ["contains", "every", "if", "in"];
@@ -30,6 +31,7 @@ impl<'source> Parser<'source> {
line: 0,
end: 0,
future_keywords: BTreeMap::new(),
rego_v1: false,
})
}
@@ -76,19 +78,19 @@ impl<'source> Parser<'source> {
pub fn set_future_keyword(&mut self, kw: &str, span: &Span) -> Result<()> {
match &self.future_keywords.get(kw) {
Some(s) if false => Err(self.source.error(
Some(s) if self.rego_v1 => Err(self.source.error(
span.line,
span.col,
format!(
"this import shadows previous import of `{kw}` defined at:{}",
self.source
.message(s.line, s.col, "", "this import is shadowed.")
s.message("", "this import is shadowed.")
)
.as_str(),
)),
_ => {
self.future_keywords.insert(kw.to_string(), span.clone());
if kw == "every" {
if kw == "every" && !self.rego_v1 {
//rego.v1 explicitly adds each keyword.
self.future_keywords.insert("in".to_string(), span.clone());
}
Ok(())
@@ -782,6 +784,17 @@ impl<'source> Parser<'source> {
span.start = start;
let op = match self.token_text() {
"=" => AssignOp::Eq,
":=" if self.rego_v1 => {
if let Expr::Var(v) = &expr {
if v.text() == "input" {
bail!(span.error("input cannot be shadowed"));
}
if v.text() == "data" {
bail!(span.error("data cannot be shadowed"));
}
}
AssignOp::ColEq
}
":=" => AssignOp::ColEq,
_ => {
*self = state;
@@ -974,6 +987,7 @@ impl<'source> Parser<'source> {
let stmt = match self.parse_literal_stmt() {
Ok(stmt) => stmt,
Err(e) if is_definite_query => return Err(e),
Err(e) if matches!(self.token_text(), "=" | ":=") => return Err(e),
Err(_) => {
// There was error parsing the first literal
// Restore the state and return.
@@ -1117,7 +1131,16 @@ impl<'source> Parser<'source> {
let span = self.tok.1.clone();
let mut term = if self.tok.0 == TokenKind::Ident {
Expr::Var(self.parse_var()?)
let v = self.parse_var()?;
if self.rego_v1 {
if v.text() == "input" {
bail!(span.error("input cannot be shadowed"));
}
if v.text() == "data" {
bail!(span.error("data cannot be shadowed"));
}
}
Expr::Var(v)
} else {
return Err(self.source.error(
span.line,
@@ -1311,6 +1334,9 @@ impl<'source> Parser<'source> {
false
}
"{" => {
if self.rego_v1 {
bail!(span.error("`if` keyword is required before rule body"));
}
self.next_token()?;
let query = Ref::new(self.parse_query(span.clone(), "}")?);
span.end = self.end;
@@ -1378,6 +1404,9 @@ impl<'source> Parser<'source> {
});
}
"{" => {
if self.rego_v1 {
bail!(span.error("`if` keyword is required before rule body"));
}
self.next_token()?;
let query = Ref::new(self.parse_query(span.clone(), "}")?);
span.end = self.end;
@@ -1463,6 +1492,25 @@ impl<'source> Parser<'source> {
let head = self.parse_rule_head()?;
let bodies = self.parse_rule_bodies()?;
span.end = self.end;
if self.rego_v1 && bodies.is_empty() {
match &head {
RuleHead::Compr { assign, .. } | RuleHead::Func { assign, .. }
if assign.is_none() =>
{
bail!(span.error("rule must have a body or assignment"));
}
RuleHead::Set { refr, key, .. } if key.is_none() => {
if Self::get_path_ref_components(refr)?.len() == 2 {
bail!(span.error("`contains` keyword is required for partial set rules"));
} else {
bail!(span.error("rule must have a body or assignment"));
}
}
_ => (),
}
}
Ok(Rule::Spec { span, head, bodies })
}
@@ -1526,15 +1574,25 @@ impl<'source> Parser<'source> {
let refr = Ref::new(self.parse_path_ref()?);
let comps = Self::get_path_ref_components(&refr)?;
if !matches!(comps[0].text(), "data" | "future" | "input") {
span.end = self.end;
if !matches!(comps[0].text(), "data" | "future" | "input" | "rego") {
return Err(self.source.error(
comps[0].line,
comps[0].col,
"import path must begin with one of: {data, future, input}",
"import path must begin with one of: {data, future, input, rego}",
));
}
let is_future_kw = self.handle_import_future_keywords(&comps)?;
let is_future_kw =
if comps.len() == 2 && comps[0].text() == "rego" && comps[1].text() == "v1" {
self.rego_v1 = true;
for kw in FUTURE_KEYWORDS {
self.set_future_keyword(kw, &span)?;
}
true
} else {
self.handle_import_future_keywords(&comps)?
};
let var = if self.token_text() == "as" {
if is_future_kw {
@@ -1588,6 +1646,7 @@ impl<'source> Parser<'source> {
package,
imports,
policy,
rego_v1: self.rego_v1,
})
}

View File

@@ -84,7 +84,6 @@ fn match_values(computed: &Value, expected: &Value) -> Result<()> {
pub fn check_output(computed_results: &[Value], expected_results: &[Value]) -> Result<()> {
if computed_results.len() != expected_results.len() {
dbg!((&computed_results, &expected_results));
bail!(
"the number of computed results ({}) and expected results ({}) is not equal",
computed_results.len(),

View File

@@ -109,7 +109,7 @@ pub fn get_path_string(refr: &Expr, document: Option<&str>) -> Result<String> {
Ok(comps.join("."))
}
pub type FunctionTable = BTreeMap<String, (Vec<Ref<Rule>>, u8)>;
pub type FunctionTable = BTreeMap<String, (Vec<Ref<Rule>>, u8, Ref<Module>)>;
fn get_extra_arg_impl(
expr: &Expr,
@@ -118,11 +118,11 @@ fn get_extra_arg_impl(
) -> Result<Option<Ref<Expr>>> {
if let Expr::Call { fcn, params, .. } = expr {
let full_path = get_path_string(fcn, module)?;
let n_args = if let Some((_, n_args)) = functions.get(&full_path) {
let n_args = if let Some((_, n_args, _)) = functions.get(&full_path) {
*n_args
} else {
let path = get_path_string(fcn, None)?;
if let Some((_, n_args)) = functions.get(&path) {
if let Some((_, n_args, _)) = functions.get(&path) {
*n_args
} else if let Some((_, n_args)) = BUILTINS.get(path.as_str()) {
*n_args
@@ -169,7 +169,7 @@ pub fn gather_functions(modules: &[Ref<Module>]) -> Result<FunctionTable> {
{
let full_path = get_path_string(refr, Some(module_path.as_str()))?;
if let Some((functions, arity)) = table.get_mut(&full_path) {
if let Some((functions, arity, _)) = table.get_mut(&full_path) {
if args.len() as u8 != *arity {
bail!(span.error(
format!("{full_path} was previously defined with {arity} arguments.")
@@ -178,7 +178,10 @@ pub fn gather_functions(modules: &[Ref<Module>]) -> Result<FunctionTable> {
}
functions.push(rule.clone());
} else {
table.insert(full_path, (vec![rule.clone()], args.len() as u8));
table.insert(
full_path,
(vec![rule.clone()], args.len() as u8, module.clone()),
);
}
}
}

View File

@@ -0,0 +1,481 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
cases:
- note: conflict future after rego.v1
data: {}
modules:
- |
package test
import rego.v1
import future.keywords.in
query: data
error: "this import shadows previous import"
- note: conflict rego after future
data: {}
modules:
- |
package test
import future.keywords.in
import rego.v1
query: data
error: "this import shadows previous import"
- note: conflict rego after rego
data: {}
modules:
- |
package test
import rego.v1
import rego.v1
query: data
error: "this import shadows previous import"
- note: allowed future after future
data: {}
modules:
- |
package test
import future.keywords
import future.keywords
query: data.test
want_result: {}
- note: if-required-before-body
data: {}
modules:
- |
package test
import rego.v1
allow {
1 > 2
}
query: data
error: "`if` keyword is required before rule body"
- note: ok-if-before-body
data: {}
modules:
- |
package test
import rego.v1
allow if {
1 < 2
}
query: data.test
want_result:
allow: true
- note: if-required-before-else-body
data: {}
modules:
- |
package test
import rego.v1
allow if {
1 > 2
} else = 5 {
1 < 2
}
query: data
error: "`if` keyword is required before rule body"
- note: ok-if-before-else-body
data: {}
modules:
- |
package test
import rego.v1
allow if {
1 > 2
} else = 5 if {
1 < 2
}
query: data.test
want_result:
allow: 5
# cases from https://www.openpolicyagent.org/docs/latest/opa-1/#backwards-compatibility-in-opa-v10
- note: invalid1
data: {}
modules:
- |
package test
import rego.v1
p { true }
query: data
error: "`if` keyword is required before rule body"
- note: invalid2
data: {}
modules:
- |
package test
import rego.v1
p.a { true }
query: data
error: "`if` keyword is required before rule body"
- note: invalid3
data: {}
modules:
- |
package test
import rego.v1
p.a.b { true }
query: data
error: "`if` keyword is required before rule body"
- note: valid1
data: {}
modules:
- |
package test
import rego.v1
p if true
query: data.test
want_result:
p: true
- note: valid2
data: {}
modules:
- |
package test
import rego.v1
p.a if true
query: data.test
want_result:
p:
a: true
- note: valid3
data: {}
modules:
- |
package test
import rego.v1
p.a.b if true
query: data.test
want_result:
p:
a:
b: true
- note: valid4
data: {}
modules:
- |
package test
import rego.v1
p contains "a"
query: data.test
want_result:
p:
set!: ["a"]
- note: valid5
data: {}
modules:
- |
package test
import rego.v1
p := 1
query: data.test
want_result:
p: 1
- note: valid6
data: {}
modules:
- |
package test
import rego.v1
p.a := 1
query: data.test
want_result:
p:
a: 1
- note: valid6
data: {}
modules:
- |
package test
import rego.v1
p.a.b := 1
query: data.test
want_result:
p:
a:
b: 1
- note: invalid11
data: {}
modules:
- |
package test
import rego.v1
p
query: data.test
error: rule must have a body
- note: invalid12
data: {}
modules:
- |
package test
import rego.v1
p.a
query: data.test
error: "`contains` keyword is required for partial set rules"
- note: invalid13
data: {}
modules:
- |
package test
import rego.v1
p.a.b
query: data.test
error: rule must have a body
- note: invalid21
data: {}
modules:
- |
package test
import rego.v1
p { true }
query: data.test
error: "`if` keyword is required before rule body"
- note: valid21
data: {}
modules:
- |
package test
import rego.v1
p if { true }
query: data.test
want_result:
p: true
- note: invalid22
data: {}
modules:
- |
package test
import rego.v1
p.a
query: data.test
error: "`contains` keyword is required for partial set rules"
- note: valid22
data: {}
modules:
- |
package test
import rego.v1
p contains "a"
query: data.test
want_result:
p:
set!: ["a"]
- note: invalid23
data: {}
modules:
- |
package test
import rego.v1
p.a { true }
query: data.test
error: "`if` keyword is required before rule body"
- note: valid22
data: {}
modules:
- |
package test
import rego.v1
p contains "a" if { true }
query: data.test
want_result:
p:
set!: ["a"]
- note: invalid24
data: {}
modules:
- |
package test
import rego.v1
p.a.b
query: data.test
error: "rule must have a body or assignment"
- note: valid22
data: {}
modules:
- |
package test
import rego.v1
p.a.b := true
query: data.test
want_result:
p:
a:
b: true
- note: invalid25
data: {}
modules:
- |
package test
import rego.v1
p.a.b { true }
query: data.test
error: "`if` keyword is required before rule body"
- note: valid22
data: {}
modules:
- |
package test
import rego.v1
p.a.b if { true }
query: data.test
want_result:
p:
a:
b: true
- note: data-shadowed-by-rule
data: {}
modules:
- |
package test
data = 1
query: data.test
want_result:
data: 1
- note: invalid-data-shadowed-by-rule
data: {}
modules:
- |
package test
import rego.v1
data = 1
query: data.test
error: data cannot be shadowed
- note: input-shadowed-by-rule
data: {}
modules:
- |
package test
input = 1
query: data.test
want_result:
input: 1
- note: invalid-input-shadowed-by-rule
data: {}
modules:
- |
package test
import rego.v1
input = 1
query: data.test
error: input cannot be shadowed
- note: input-shadowed-by-local-var
data: {}
modules:
- |
package test
x {
input := 1
input > 0
}
y {
# This evaluates to false
input = 1
input > 0
}
query: data.test
want_result:
x: true
- note: invalid-input-shadowed-by-local-var
data: {}
modules:
- |
package test
import rego.v1
x if {
input := 1
input > 0
}
query: data.test
error: input cannot be shadowed
- note: data-shadowed-by-local-var
data: {}
modules:
- |
package test
x {
data := 1
data > 0
}
query: data.test
want_result:
x: true
- note: invalid-data-shadowed-by-local-var
data: {}
modules:
- |
package test
import rego.v1
x if {
data := 1
data > 0
}
query: data.test
error: data cannot be shadowed
- note: deprecated-function
data: {}
modules:
- |
package test
x {
cast_array([1])
}
query: data.test
want_result:
x: true
- note: invalid-deprecated-function
data: {}
modules:
- |
package test
import rego.v1
x if {
cast_array([1])
}
query: data.test
error: is deprecated

View File

@@ -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.60.0";
const OPA_BRANCH: &str = "v0.61.0";
#[derive(Serialize, Deserialize, PartialEq, Debug)]
#[serde(deny_unknown_fields)]

View File

@@ -224,13 +224,13 @@ cases:
rego: |
package test
import foo
error: "import path must begin with one of: {data, future, input}"
error: "import path must begin with one of: {data, future, input, rego}"
- note: invalid-beginning-1
rego: |
package test
import foo.bar
error: "import path must begin with one of: {data, future, input}"
error: "import path must begin with one of: {data, future, input, rego}"
- note: missing-field-1
rego: |