Compare commits

...

19 Commits

Author SHA1 Message Date
Anand Krishnamoorthi
bb1b25ff2f chore: release (#137)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-02-08 07:09:22 -08:00
Anand Krishnamoorthi
5fd826d79a Update docs (#136)
* Add `time` to opa.passing. Disable WASM from rust.yml

Bindings will be tested using a separate workflow.
Also remove scripts that are no longer useful

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

* Remove alpha tag from version

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

---------

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2024-02-08 06:56:14 -08:00
Anand Krishnamoorthi
d4dcbe7b9e Update README. Add link to playground. (#135)
Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2024-02-07 22:18:31 -08:00
Anand Krishnamoorthi
fc68bf9c8b Add devcontainer (#133)
Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2024-02-07 15:40:50 -08:00
Anand Krishnamoorthi
a95a9d21b3 Ability to add custom builtin functions (#132)
Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2024-02-07 15:15:55 -08:00
Burak
5717f9c249 Partially implement Go's time format (#130)
* Partially implement Go's time format

* Parse date only values

* Fix leap year handling in `time.diff`

* Disable failing test case
2024-02-07 15:12:34 -08:00
Anand Krishnamoorthi
dda525b989 C++ binding (#129)
Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2024-02-05 00:22:27 -08:00
Anand Krishnamoorthi
22260ac46f Bindings for C, C#, Golang (#124)
* FFI bindings

Generate C FFI as well as C# FFI

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

* Regorus C binding

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

* C# binding

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

* Golang binding

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

---------

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2024-02-04 19:50:44 -08:00
dependabot[bot]
25b1ffe6d7 Update env_logger requirement from 0.10.0 to 0.11.1 (#123)
Updates the requirements on [env_logger](https://github.com/rust-cli/env_logger) to permit the latest version.
- [Release notes](https://github.com/rust-cli/env_logger/releases)
- [Changelog](https://github.com/rust-cli/env_logger/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-cli/env_logger/compare/v0.10.0...v0.11.1)

---
updated-dependencies:
- dependency-name: env_logger
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-02 16:17:16 -08:00
Anand Krishnamoorthi
bcde71b8f0 Create dependabot.yml 2024-02-02 14:54:58 -08:00
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
63 changed files with 4662 additions and 114 deletions

View File

@@ -0,0 +1,35 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/rust
{
"name": "Rust",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/rust:1-1-bullseye",
"features": {
"ghcr.io/devcontainers/features/dotnet:2": {},
"ghcr.io/devcontainers/features/python:1": {}
}
// Use 'mounts' to make the cargo cache persistent in a Docker Volume.
// "mounts": [
// {
// "source": "devcontainer-cargo-cache-${devcontainerId}",
// "target": "/usr/local/cargo",
// "type": "volume"
// }
// ]
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "rustc --version",
// Configure tool-specific properties.
// "customizations": {},
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}

11
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "cargo"
directory: "/" # Location of package manifests
schedule:
interval: "weekly"

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

@@ -36,4 +36,12 @@ jobs:
run: cargo test -r --test aci
- name: Run tests (OPA Conformance)
run: >-
cargo test -r --test opa -- $(tr '\n' ' ' < tests/opa.passing)
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

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,15 @@
[workspace]
members = [
"bindings/ffi",
"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"
edition = "2021"
license-file = "LICENSE"
repository = "https://github.com/microsoft/regorus"
@@ -58,7 +66,7 @@ serde = {version = "1.0.150", features = ["derive", "rc"] }
serde_json = {version = "1.0.89", features = ["arbitrary_precision"] }
serde_yaml = {version = "0.9.16", optional = true }
log = "0.4.17"
env_logger="0.10.0"
env_logger="0.11.1"
lazy_static = "1.4.0"
rand = "0.8.5"
num = "0.4.1"
@@ -82,7 +90,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

@@ -6,8 +6,21 @@
interpreter written in Rust.
- *Rigorous* - A rigorous enforcer of well-defined Rego semantics.
Regorus is available as a library that can be easily integrated into your Rust projects.
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.
- *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*.
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/).
Regorus is available as a library that can be easily integrated into your Rust projects.
Here is an example of evaluating a simple Rego policy:
```rust
@@ -40,13 +53,12 @@ fn main() -> Result<()> {
```
Regorus is designed with [Confidential Computing](https://confidentialcomputing.io/about/) in mind. In Confidential Computing environments,
it is important to be able to control exactly what is being run. Regorus allows enabling and disabling various components using cargo
it is important to be able to control exactly what is being run. Regorus allows enabling and disabling various components using cargo
features. By default all features are enabled.
The default build of regorus example program is 6.4M:
```bash
$ cargo build -r --example regorus; strip target/release/examples/regorus; ls -lh target/release/examples/regorus
$ cargo build -r --example regorus; strip target/release/examples/regorus; ls -lh target/release/examples/regorus
-rwxr-xr-x 1 anand staff 6.4M Jan 19 11:23 target/release/examples/regorus*
```
@@ -57,10 +69,30 @@ $ cargo build -r --example regorus --features "yaml" --no-default-features; stri
-rwxr-xr-x 1 anand staff 2.9M Jan 19 11:26 target/release/examples/regorus*
```
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:
- *C*: C binding is generated using [cbindgen](https://github.com/mozilla/cbindgen).
[corrosion-rs](https://github.com/corrosion-rs/corrosion) can be used to seamlessly use Regorous
in your CMake based projects. See [bindings/c](https://github.com/microsoft/regorus/tree/main/bindings/c).
- *C++*: C++ binding is generated using [cbindgen](https://github.com/mozilla/cbindgen).
[corrosion-rs](https://github.com/corrosion-rs/corrosion) can be used to seamlessly use Regorous
in your CMake based projects. See [bindings/cpp](https://github.com/microsoft/regorus/tree/main/bindings/cpp).
- *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).
- *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/).
To avoid operational overhead, we currently don't publish these bindings to various repositories.
It is straight-forward to build these bindings yourself.
## Getting Started
[examples/regorus](https://github.com/microsoft/regorus/blob/main/examples/regorus.rs) is an example program that
@@ -192,7 +224,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.
@@ -237,7 +269,6 @@ The following test suites don't pass fully due to mising builtins:
- `regometadatarule`
- `regoparsemodule`
- `rendertemplate`
- `time`
They are captured in the following [github issues](https://github.com/microsoft/regorus/issues?q=is%3Aopen+is%3Aissue+label%3Alib).

27
bindings/c/CMakeLists.txt Normal file
View File

@@ -0,0 +1,27 @@
# Copyright (c) Microsoft
# Licensed under the MIT License.
cmake_minimum_required(VERSION 3.12 FATAL_ERROR)
include(FetchContent)
FetchContent_Declare(
Corrosion
GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git
GIT_TAG v0.4 # Optionally specify a commit hash, version tag or branch here
)
FetchContent_MakeAvailable(Corrosion)
project("regorus-test")
corrosion_import_crate(
# Path to <regorus-source-folder>/bindings/ffi/Cargo.toml
MANIFEST_PATH "../ffi/Cargo.toml"
# Always build regorus in Release mode.
PROFILE "release"
# Only build the "regorusc" crate.
CRATES "regorus-ffi")
add_executable(regorus_test main.c)
# Add path to <regorus-source-folder>/bindings/ffi
target_include_directories(regorus_test PRIVATE "../ffi")
target_link_libraries(regorus_test regorus-ffi)

55
bindings/c/main.c Normal file
View File

@@ -0,0 +1,55 @@
#include <stdio.h>
#include "regorus.h"
int main() {
// Create engine.
RegorusEngine* engine = regorus_engine_new();
RegorusResult r;
// Load policies.
r = regorus_engine_add_policy_from_file(engine, "../../../tests/aci/framework.rego");
if (r.status != RegorusStatusOk)
goto error;
regorus_result_drop(r);
r = regorus_engine_add_policy_from_file(engine, "../../../tests/aci/api.rego");
if (r.status != RegorusStatusOk)
goto error;
regorus_result_drop(r);
r = regorus_engine_add_policy_from_file(engine, "../../../tests/aci/policy.rego");
if (r.status != RegorusStatusOk)
goto error;
regorus_result_drop(r);
// Add data
r = regorus_engine_add_data_from_json_file(engine, "../../../tests/aci/data.json");
if (r.status != RegorusStatusOk)
goto error;
regorus_result_drop(r);
// Set input
r = regorus_engine_set_input_from_json_file(engine, "../../../tests/aci/input.json");
if (r.status != RegorusStatusOk)
goto error;
regorus_result_drop(r);
// Eval query
r = regorus_engine_eval_query(engine, "data.framework.mount_overlay=x");
if (r.status != RegorusStatusOk)
goto error;
// Print output
printf("%s", r.output);
regorus_result_drop(r);
// Free the engine.
regorus_engine_drop(engine);
return 0;
error:
printf("%s", r.error_message);
return 1;
}

View File

@@ -0,0 +1,28 @@
# Copyright (c) Microsoft
# Licensed under the MIT License.
cmake_minimum_required(VERSION 3.12 FATAL_ERROR)
include(FetchContent)
FetchContent_Declare(
Corrosion
GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git
GIT_TAG v0.4 # Optionally specify a commit hash, version tag or branch here
)
FetchContent_MakeAvailable(Corrosion)
project("regorus-test")
set(CMAKE_CXX_STANDARD 17)
corrosion_import_crate(
# Path to <regorus-source-folder>/bindings/ffi/Cargo.toml
MANIFEST_PATH "../ffi/Cargo.toml"
# Always build regorus in Release mode.
PROFILE "release"
# Only build the "regorusc" crate.
CRATES "regorus-ffi")
add_executable(regorus_test main.cpp)
# Add path to <regorus-source-folder>/bindings/ffi
target_include_directories(regorus_test PRIVATE "../ffi")
target_link_libraries(regorus_test regorus-ffi)

118
bindings/cpp/main.cpp Normal file
View File

@@ -0,0 +1,118 @@
#include <iostream>
#include "regorus.hpp"
void example()
{
// Create engine
regorus::Engine engine;
// Add policies.
engine.add_policy("objects.rego",R"(package objects
rect := {`width`: 2, "height": 4}
cube := {"width": 3, `height`: 4, "depth": 5}
a := 42
b := false
c := null
d := {"a": a, "x": [b, c]}
index := 1
shapes := [rect, cube]
names := ["prod", `smoke1`, "dev"]
sites := [{"name": "prod"}, {"name": names[index]}, {"name": "dev"}]
e := {
a: "foo",
"three": c,
names[2]: b,
"four": d,
}
f := e["dev"])");
// Add data.
engine.add_data_json(R"({
"one": {
"bar": "Foo",
"baz": 5,
"be": true,
"bop": 23.4
},
"two": {
"bar": "Bar",
"baz": 12.3,
"be": false,
"bop": 42
}
})");
engine.add_data_json(R"({
"three": {
"bar": "Baz",
"baz": 15,
"be": true,
"bop": 4.23
}
})");
// Set input.
engine.set_input_json(R"({
"a": 10,
"b": "20",
"c": 30.0,
"d": true
})");
// Eval query.
auto result = engine.eval_query("[data.one, input.b, data.objects.sites[1]] = x");
if (result) {
std::cout<<result.output()<<std::endl;
} else {
std::cerr<<result.error()<<std::endl;
}
}
int main() {
// Create engine.
regorus::Engine engine;
// Load policies.
const char* policies[] = {
"../../../tests/aci/framework.rego",
"../../../tests/aci/policy.rego",
"../../../tests/aci/api.rego",
};
// Add policies and data.
for (auto policy : policies) {
auto result = engine.add_policy_from_file(policy);
if (!result) {
std::cerr<<result.error()<<std::endl;
return -1;
}
}
{
auto result = engine.add_data_from_json_file("../../../tests/aci/data.json");
if (!result) {
std::cerr<<result.error()<<std::endl;
return -1;
}
}
// Set input and eval query.
{
auto result = engine.set_input_from_json_file("../../../tests/aci/input.json");
if (!result) {
std::cerr<<result.error()<<std::endl;
return -1;
}
}
auto result = engine.eval_query("data.framework.mount_overlay = x");
if (!result) {
std::cerr<<result.error()<<std::endl;
return -1;
}
std::cout<<result.output()<<std::endl;
example();
}

101
bindings/cpp/regorus.hpp Normal file
View File

@@ -0,0 +1,101 @@
#ifndef REGORUS_WRAPPER_HPP
#define REGORUS_WRAPPER_HPP
#include <memory>
#include <variant>
#include "regorus.ffi.hpp"
namespace regorus {
class Result {
public:
operator bool() const { return result.status == RegorusStatus::RegorusStatusOk; }
bool operator !() const { return result.status != RegorusStatus::RegorusStatusOk; }
const char* output() const {
if (*this && result.output) {
return result.output;
} else {
return "";
}
}
const char* error() const {
if (!*this && result.error_message) {
return result.error_message;
} else {
return "";
}
}
~Result() {
regorus_result_drop(result);
}
private:
friend class Engine;
RegorusResult result;
Result(RegorusResult r) : result(r) {}
private:
Result(const Result&) = delete;
Result(Result&&) = delete;
Result& operator=(const Result&) = delete;
};
class Engine {
public:
Engine() : Engine(regorus_engine_new()) {}
std::unique_ptr<Engine> clone() const {
return std::unique_ptr<Engine>(new Engine(regorus_engine_clone(engine)));
}
Result add_policy(const char* path, const char* policy) {
return Result(regorus_engine_add_policy(engine, path, policy));
}
Result add_policy_from_file(const char* path) {
return Result(regorus_engine_add_policy_from_file(engine, path));
}
Result add_data_json(const char* data) {
return Result(regorus_engine_add_data_json(engine, data));
}
Result add_data_from_json_file(const char* path) {
return Result(regorus_engine_add_data_from_json_file(engine, path));
}
Result set_input_json(const char* input) {
return Result(regorus_engine_set_input_json(engine, input));
}
Result set_input_from_json_file(const char* path) {
return Result(regorus_engine_set_input_from_json_file(engine, path));
}
Result eval_query(const char* query) {
return Result(regorus_engine_eval_query(engine, query));
}
~Engine() {
regorus_engine_drop(engine);
}
private:
RegorusEngine* engine;
private:
Engine(RegorusEngine* e) : engine(e) {}
Engine(const Engine&) = delete;
Engine(Engine&&) = delete;
Engine& operator=(const Engine&) = delete;
};
}
#endif // REGORUS_WRAPPER_HPP

View File

@@ -0,0 +1,51 @@
using System.Diagnostics;
long nanosecPerTick = (1000L*1000L*1000L) / Stopwatch.Frequency;
var w = new Stopwatch();
// Force load of modules.
{
var _e = new Regorus.Engine();
var _j = System.Text.Json.JsonDocument.Parse("{}");
}
w.Restart();
var engine = new Regorus.Engine();
w.Stop();
var newEngineTicks = w.ElapsedTicks;
w.Restart();
// Load policies and data.
engine.AddPolicyFromFile("../../tests/aci/framework.rego");
engine.AddPolicyFromFile("../../tests/aci/api.rego");
engine.AddPolicyFromFile("../../tests/aci/policy.rego");
engine.AddDataFromJsonFile("../../tests/aci/data.json");
w.Stop();
var loadPoliciesTicks = w.ElapsedTicks;
w.Restart();
// Set input and eval query.
engine.SetInputFromJsonFile("../../tests/aci/input.json");
var results = engine.EvalQuery("data.framework.mount_overlay = x");
var resultsDoc = System.Text.Json.JsonDocument.Parse(results);
w.Stop();
var evalTicks = w.ElapsedTicks;
Console.WriteLine("{0}", results);
Console.WriteLine("Engine creation took {0} msecs", (newEngineTicks*nanosecPerTick)/(1000.0*1000.0));
Console.WriteLine("Load policies and data took {0} msecs", (loadPoliciesTicks*nanosecPerTick)/(1000.0*1000.0));
Console.WriteLine("EvalQuery took {0} msecs", (evalTicks*nanosecPerTick)/(1000.0*1000.0));

169
bindings/csharp/Regorus.cs Normal file
View File

@@ -0,0 +1,169 @@
using System.Text;
namespace Regorus
{
public class Exception : System.Exception
{
public Exception(string? message) : base(message) {}
}
public class Engine : ICloneable
{
unsafe private RegorusFFI.RegorusEngine* E;
public Engine()
{
unsafe
{
E = RegorusFFI.API.regorus_engine_new();
}
}
public object Clone()
{
var clone = (Engine)this.MemberwiseClone();
unsafe
{
clone.E = RegorusFFI.API.regorus_engine_clone(E);
}
return clone;
}
public void AddPolicy(string path, string rego)
{
var pathBytes = Encoding.UTF8.GetBytes(path);
var regoBytes = Encoding.UTF8.GetBytes(rego);
unsafe
{
fixed (byte* pathPtr = pathBytes)
{
fixed(byte* regoPtr = regoBytes)
{
CheckAndDropResult(RegorusFFI.API.regorus_engine_add_policy(E, pathPtr, regoPtr));
}
}
}
}
public void AddPolicyFromFile(string path)
{
var pathBytes = Encoding.UTF8.GetBytes(path);
unsafe
{
fixed (byte* pathPtr = pathBytes)
{
CheckAndDropResult(RegorusFFI.API.regorus_engine_add_policy_from_file(E, pathPtr));
}
}
}
public void AddDataJson(string data)
{
var dataBytes = Encoding.UTF8.GetBytes(data);
unsafe
{
fixed (byte* dataPtr = dataBytes)
{
CheckAndDropResult(RegorusFFI.API.regorus_engine_add_data_json(E, dataPtr));
}
}
}
public void AddDataFromJsonFile(string path)
{
var pathBytes = Encoding.UTF8.GetBytes(path);
unsafe
{
fixed (byte* pathPtr = pathBytes)
{
CheckAndDropResult(RegorusFFI.API.regorus_engine_add_data_from_json_file(E, pathPtr));
}
}
}
public void SetInputJson(string input)
{
var inputBytes = Encoding.UTF8.GetBytes(input);
unsafe
{
fixed (byte* inputPtr = inputBytes)
{
CheckAndDropResult(RegorusFFI.API.regorus_engine_set_input_json(E, inputPtr));
}
}
}
public void SetInputFromJsonFile(string path)
{
var pathBytes = Encoding.UTF8.GetBytes(path);
unsafe
{
fixed (byte* pathPtr = pathBytes)
{
CheckAndDropResult(RegorusFFI.API.regorus_engine_set_input_from_json_file(E, pathPtr));
}
}
}
public string EvalQuery(string query)
{
var queryBytes = Encoding.UTF8.GetBytes(query);
var resultJson = "";
unsafe
{
fixed (byte* queryPtr = queryBytes)
{
var result = RegorusFFI.API.regorus_engine_eval_query(E, queryPtr);
if (result.status == RegorusFFI.RegorusStatus.RegorusStatusOk) {
if (result.output is not null) {
resultJson = System.Runtime.InteropServices.Marshal.PtrToStringUTF8((IntPtr)result.output);
}
RegorusFFI.API.regorus_result_drop(result);
} else {
CheckAndDropResult(result);
}
}
}
if (resultJson is not null) {
return resultJson;
} else {
return "";
}
}
~Engine()
{
unsafe
{
RegorusFFI.API.regorus_engine_drop(E);
}
}
void CheckAndDropResult(RegorusFFI.RegorusResult result)
{
if (result.status != RegorusFFI.RegorusStatus.RegorusStatusOk) {
unsafe {
var message = System.Runtime.InteropServices.Marshal.PtrToStringUTF8((IntPtr)result.error_message);
var ex = new Exception(message);
RegorusFFI.API.regorus_result_drop(result);
throw ex;
}
}
RegorusFFI.API.regorus_result_drop(result);
}
}
}

View File

@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<Target Name="BuildRegorusFFI">
<Exec Command="cargo build -r --manifest-path ../ffi/Cargo.toml" />
<Copy SourceFiles="../ffi/RegorusFFI.g.cs" DestinationFolder="." />
<ItemGroup>
<RegorusDylib Include="..\..\target\release\*regorus_ffi*" />
</ItemGroup>
<Copy SourceFiles="@(RegorusDylib)" DestinationFolder="." />
</Target>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>regorus_test</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
</Project>

13
bindings/ffi/CHANGELOG.md Normal file
View 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-ffi-v0.1.0) - 2024-02-08
### Other
- C++ binding ([#129](https://github.com/microsoft/regorus/pull/129))
- Bindings for C, C#, Golang ([#124](https://github.com/microsoft/regorus/pull/124))

17
bindings/ffi/Cargo.toml Normal file
View File

@@ -0,0 +1,17 @@
[package]
name = "regorus-ffi"
version = "0.1.0"
edition = "2021"
# 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"
regorus = { path = "../.." }
serde_json = "1.0.113"
[build-dependencies]
cbindgen = "0.26.0"
csbindgen = "1.9.0"

View File

@@ -0,0 +1,88 @@
// <auto-generated>
// This code is generated by csbindgen.
// DON'T CHANGE THIS DIRECTLY.
// </auto-generated>
#pragma warning disable CS8500
#pragma warning disable CS8981
using System;
using System.Runtime.InteropServices;
namespace RegorusFFI
{
internal static unsafe partial class API
{
const string __DllName = "regorusc";
/// <summary>Drop a `RegorusResult`. `output` and `error_message` strings are not valid after drop.</summary>
[DllImport(__DllName, EntryPoint = "regorus_result_drop", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
public static extern void regorus_result_drop(RegorusResult r);
/// <summary>Construct a new Engine See https://docs.rs/regorus/latest/regorus/struct.Engine.html</summary>
[DllImport(__DllName, EntryPoint = "regorus_engine_new", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
public static extern RegorusEngine* regorus_engine_new();
/// <summary>Clone a [`RegorusEngine`] To avoid having to parse same policy again, the engine can be cloned after policies and data have been added.</summary>
[DllImport(__DllName, EntryPoint = "regorus_engine_clone", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
public static extern RegorusEngine* regorus_engine_clone(RegorusEngine* engine);
[DllImport(__DllName, EntryPoint = "regorus_engine_drop", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
public static extern void regorus_engine_drop(RegorusEngine* engine);
/// <summary>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.</summary>
[DllImport(__DllName, EntryPoint = "regorus_engine_add_policy", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
public static extern RegorusResult regorus_engine_add_policy(RegorusEngine* engine, byte* path, byte* rego);
[DllImport(__DllName, EntryPoint = "regorus_engine_add_policy_from_file", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
public static extern RegorusResult regorus_engine_add_policy_from_file(RegorusEngine* engine, byte* path);
/// <summary>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.</summary>
[DllImport(__DllName, EntryPoint = "regorus_engine_add_data_json", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
public static extern RegorusResult regorus_engine_add_data_json(RegorusEngine* engine, byte* data);
[DllImport(__DllName, EntryPoint = "regorus_engine_add_data_from_json_file", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
public static extern RegorusResult regorus_engine_add_data_from_json_file(RegorusEngine* engine, byte* path);
/// <summary>Clear policy data. See https://docs.rs/regorus/0.1.0-alpha.2/regorus/struct.Engine.html#method.clear_data</summary>
[DllImport(__DllName, EntryPoint = "regorus_engine_clear_data", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
public static extern RegorusResult regorus_engine_clear_data(RegorusEngine* engine);
/// <summary>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.</summary>
[DllImport(__DllName, EntryPoint = "regorus_engine_set_input_json", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
public static extern RegorusResult regorus_engine_set_input_json(RegorusEngine* engine, byte* input);
[DllImport(__DllName, EntryPoint = "regorus_engine_set_input_from_json_file", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
public static extern RegorusResult regorus_engine_set_input_from_json_file(RegorusEngine* engine, byte* path);
/// <summary>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.</summary>
[DllImport(__DllName, EntryPoint = "regorus_engine_eval_query", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
public static extern RegorusResult regorus_engine_eval_query(RegorusEngine* engine, byte* query);
}
[StructLayout(LayoutKind.Sequential)]
internal unsafe partial struct RegorusResult
{
public RegorusStatus status;
public byte* output;
public byte* error_message;
}
[StructLayout(LayoutKind.Sequential)]
internal unsafe partial struct RegorusEngine
{
}
internal enum RegorusStatus : uint
{
RegorusStatusOk,
RegorusStatusError,
}
}

32
bindings/ffi/build.rs Normal file
View File

@@ -0,0 +1,32 @@
extern crate cbindgen;
extern crate csbindgen;
use std::env;
fn main() {
let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
cbindgen::Builder::new()
.with_crate(&crate_dir)
.with_language(cbindgen::Language::C)
.with_include_guard("REGORUS_H")
.generate()
.expect("Unable to generate bindings")
.write_to_file("regorus.h");
cbindgen::Builder::new()
.with_crate(crate_dir)
.with_language(cbindgen::Language::Cxx)
.with_include_guard("REGORUS_FFI_HPP")
.generate()
.expect("Unable to generate bindings")
.write_to_file("regorus.ffi.hpp");
csbindgen::Builder::default()
.input_extern_file("src/lib.rs")
.csharp_dll_name("regorusc")
.csharp_class_name("API")
.csharp_namespace("RegorusFFI")
.generate_csharp_file("./RegorusFFI.g.cs")
.unwrap();
}

158
bindings/ffi/cbindgen.toml Normal file
View File

@@ -0,0 +1,158 @@
# This is a template cbindgen.toml file with all of the default values.
# Some values are commented out because their absence is the real default.
#
# See https://github.com/mozilla/cbindgen/blob/master/docs.md#cbindgentoml
# for detailed documentation of every option here.
language = "C++"
############## Options for Wrapping the Contents of the Header #################
# header = "/* Text to put at the beginning of the generated file. Probably a license. */"
# trailer = "/* Text to put at the end of the generated file */"
# include_guard = "my_bindings_h"
# pragma_once = true
# autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */"
include_version = false
# namespace = "my_namespace"
namespaces = []
using_namespaces = []
sys_includes = []
includes = []
no_includes = false
after_includes = ""
############################ Code Style Options ################################
braces = "SameLine"
line_length = 100
tab_width = 2
documentation = true
documentation_style = "auto"
documentation_length = "full"
line_endings = "LF" # also "CR", "CRLF", "Native"
############################# Codegen Options ##################################
style = "both"
sort_by = "Name" # default for `fn.sort_by` and `const.sort_by`
usize_is_size_t = true
[defines]
# "target_os = freebsd" = "DEFINE_FREEBSD"
# "feature = serde" = "DEFINE_SERDE"
[export]
include = []
exclude = []
# prefix = "CAPI_"
item_types = []
renaming_overrides_prefixing = false
[export.rename]
[export.body]
[export.mangle]
[fn]
rename_args = "None"
# must_use = "MUST_USE_FUNC"
# deprecated = "DEPRECATED_FUNC"
# deprecated_with_note = "DEPRECATED_FUNC_WITH_NOTE"
# no_return = "NO_RETURN"
# prefix = "START_FUNC"
# postfix = "END_FUNC"
args = "auto"
sort_by = "Name"
[struct]
rename_fields = "None"
# must_use = "MUST_USE_STRUCT"
# deprecated = "DEPRECATED_STRUCT"
# deprecated_with_note = "DEPRECATED_STRUCT_WITH_NOTE"
derive_constructor = false
derive_eq = false
derive_neq = false
derive_lt = false
derive_lte = false
derive_gt = false
derive_gte = false
[enum]
rename_variants = "None"
# must_use = "MUST_USE_ENUM"
# deprecated = "DEPRECATED_ENUM"
# deprecated_with_note = "DEPRECATED_ENUM_WITH_NOTE"
add_sentinel = false
prefix_with_name = false
derive_helper_methods = false
derive_const_casts = false
derive_mut_casts = false
# cast_assert_name = "ASSERT"
derive_tagged_enum_destructor = false
derive_tagged_enum_copy_constructor = false
enum_class = true
private_default_tagged_enum_constructor = false
[const]
allow_static_const = true
allow_constexpr = false
sort_by = "Name"
[macro_expansion]
bitflags = false
############## Options for How Your Rust library Should Be Parsed ##############
[parse]
parse_deps = false
# include = []
exclude = []
clean = false
extra_bindings = []
[parse.expand]
crates = []
all_features = false
default_features = true
features = []

View File

@@ -0,0 +1,95 @@
#ifndef REGORUS_FFI_HPP
#define REGORUS_FFI_HPP
#include <cstdarg>
#include <cstdint>
#include <cstdlib>
#include <ostream>
#include <new>
/// Status of a call on `RegorusEngine`.
enum class RegorusStatus {
/// The operation was successful.
RegorusStatusOk,
/// The operation was unsuccessful.
RegorusStatusError,
};
/// Wrapper for `regorus::Engine`.
struct RegorusEngine;
/// Result of a call on `RegorusEngine`.
///
/// Must be freed using `regorus_result_drop`.
struct RegorusResult {
/// Status
RegorusStatus status;
/// Output produced by the call.
/// Owned by Rust.
char *output;
/// Errors produced by the call.
/// Owned by Rust.
char *error_message;
};
extern "C" {
/// Drop a `RegorusResult`.
///
/// `output` and `error_message` strings are not valid after drop.
void regorus_result_drop(RegorusResult r);
/// Construct a new Engine
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html
RegorusEngine *regorus_engine_new();
/// Clone a [`RegorusEngine`]
///
/// To avoid having to parse same policy again, the engine can be cloned
/// after policies and data have been added.
RegorusEngine *regorus_engine_clone(RegorusEngine *engine);
void regorus_engine_drop(RegorusEngine *engine);
/// 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.
RegorusResult regorus_engine_add_policy(RegorusEngine *engine, const char *path, const char *rego);
RegorusResult regorus_engine_add_policy_from_file(RegorusEngine *engine, const char *path);
/// 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.
RegorusResult regorus_engine_add_data_json(RegorusEngine *engine, const char *data);
RegorusResult regorus_engine_add_data_from_json_file(RegorusEngine *engine, const char *path);
/// Clear policy data.
///
/// See https://docs.rs/regorus/0.1.0-alpha.2/regorus/struct.Engine.html#method.clear_data
RegorusResult regorus_engine_clear_data(RegorusEngine *engine);
/// 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.
RegorusResult regorus_engine_set_input_json(RegorusEngine *engine, const char *input);
RegorusResult regorus_engine_set_input_from_json_file(RegorusEngine *engine, const char *path);
/// 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.
RegorusResult regorus_engine_eval_query(RegorusEngine *engine, const char *query);
} // extern "C"
#endif // REGORUS_FFI_HPP

127
bindings/ffi/regorus.h Normal file
View File

@@ -0,0 +1,127 @@
#ifndef REGORUS_H
#define REGORUS_H
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
/**
* Status of a call on `RegorusEngine`.
*/
typedef enum RegorusStatus {
/**
* The operation was successful.
*/
RegorusStatusOk,
/**
* The operation was unsuccessful.
*/
RegorusStatusError,
} RegorusStatus;
/**
* Wrapper for `regorus::Engine`.
*/
typedef struct RegorusEngine RegorusEngine;
/**
* Result of a call on `RegorusEngine`.
*
* Must be freed using `regorus_result_drop`.
*/
typedef struct RegorusResult {
/**
* Status
*/
enum RegorusStatus status;
/**
* Output produced by the call.
* Owned by Rust.
*/
char *output;
/**
* Errors produced by the call.
* Owned by Rust.
*/
char *error_message;
} RegorusResult;
/**
* Drop a `RegorusResult`.
*
* `output` and `error_message` strings are not valid after drop.
*/
void regorus_result_drop(struct RegorusResult r);
/**
* Construct a new Engine
*
* See https://docs.rs/regorus/latest/regorus/struct.Engine.html
*/
struct RegorusEngine *regorus_engine_new(void);
/**
* Clone a [`RegorusEngine`]
*
* To avoid having to parse same policy again, the engine can be cloned
* after policies and data have been added.
*/
struct RegorusEngine *regorus_engine_clone(struct RegorusEngine *engine);
void regorus_engine_drop(struct RegorusEngine *engine);
/**
* 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.
*/
struct RegorusResult regorus_engine_add_policy(struct RegorusEngine *engine,
const char *path,
const char *rego);
struct RegorusResult regorus_engine_add_policy_from_file(struct RegorusEngine *engine,
const char *path);
/**
* 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.
*/
struct RegorusResult regorus_engine_add_data_json(struct RegorusEngine *engine, const char *data);
struct RegorusResult regorus_engine_add_data_from_json_file(struct RegorusEngine *engine,
const char *path);
/**
* Clear policy data.
*
* See https://docs.rs/regorus/0.1.0-alpha.2/regorus/struct.Engine.html#method.clear_data
*/
struct RegorusResult regorus_engine_clear_data(struct RegorusEngine *engine);
/**
* 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.
*/
struct RegorusResult regorus_engine_set_input_json(struct RegorusEngine *engine, const char *input);
struct RegorusResult regorus_engine_set_input_from_json_file(struct RegorusEngine *engine,
const char *path);
/**
* 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.
*/
struct RegorusResult regorus_engine_eval_query(struct RegorusEngine *engine, const char *query);
#endif /* REGORUS_H */

248
bindings/ffi/src/lib.rs Normal file
View File

@@ -0,0 +1,248 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use anyhow::{anyhow, bail, Result};
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
/// Status of a call on `RegorusEngine`.
#[repr(C)]
pub enum RegorusStatus {
/// The operation was successful.
RegorusStatusOk,
/// The operation was unsuccessful.
RegorusStatusError,
}
/// Result of a call on `RegorusEngine`.
///
/// Must be freed using `regorus_result_drop`.
#[repr(C)]
pub struct RegorusResult {
/// Status
status: RegorusStatus,
/// Output produced by the call.
/// Owned by Rust.
output: *mut c_char,
/// Errors produced by the call.
/// Owned by Rust.
error_message: *mut c_char,
}
fn to_c_str(s: String) -> *mut c_char {
match CString::new(s) {
Ok(cs) => cs.into_raw(),
_ => to_c_str("binding error: failed to create c-style string".to_string()),
}
}
fn from_c_str(s: *const c_char) -> Result<String> {
if s.is_null() {
bail!("null pointer");
}
unsafe {
CStr::from_ptr(s)
.to_str()
.map_err(|_| anyhow!("`path`: invalid utf8"))
.map(|s| s.to_string())
}
}
fn to_ref<T>(t: &*mut T) -> Result<&mut T> {
unsafe { t.as_mut().ok_or_else(|| anyhow!("null pointer")) }
}
fn to_regorus_result(r: Result<()>) -> RegorusResult {
match r {
Ok(()) => RegorusResult {
status: RegorusStatus::RegorusStatusOk,
output: std::ptr::null_mut(),
error_message: std::ptr::null_mut(),
},
Err(e) => RegorusResult {
status: RegorusStatus::RegorusStatusError,
output: std::ptr::null_mut(),
error_message: to_c_str(format!("{e}")),
},
}
}
/// Wrapper for `regorus::Engine`.
#[derive(Clone)]
pub struct RegorusEngine {
engine: ::regorus::Engine,
}
/// Drop a `RegorusResult`.
///
/// `output` and `error_message` strings are not valid after drop.
#[no_mangle]
pub extern "C" fn regorus_result_drop(r: RegorusResult) {
if !r.error_message.is_null() {
unsafe {
let _ = CString::from_raw(r.error_message);
}
}
}
#[no_mangle]
/// Construct a new Engine
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html
pub extern "C" fn regorus_engine_new() -> *mut RegorusEngine {
let engine = ::regorus::Engine::new();
Box::into_raw(Box::new(RegorusEngine { engine }))
}
/// Clone a [`RegorusEngine`]
///
/// To avoid having to parse same policy again, the engine can be cloned
/// after policies and data have been added.
#[no_mangle]
pub extern "C" fn regorus_engine_clone(engine: *mut RegorusEngine) -> *mut RegorusEngine {
unsafe {
if engine.is_null() {
return std::ptr::null_mut();
}
Box::into_raw(Box::new((*engine).clone()))
}
}
#[no_mangle]
pub extern "C" fn regorus_engine_drop(engine: *mut RegorusEngine) {
if !engine.is_null() {
unsafe {
let _ = Box::from_raw(engine);
}
}
}
/// 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.
#[no_mangle]
pub extern "C" fn regorus_engine_add_policy(
engine: *mut RegorusEngine,
path: *const c_char,
rego: *const c_char,
) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
to_ref(&engine)?
.engine
.add_policy(from_c_str(path)?, from_c_str(rego)?)
}())
}
#[no_mangle]
pub extern "C" fn regorus_engine_add_policy_from_file(
engine: *mut RegorusEngine,
path: *const c_char,
) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
to_ref(&engine)?
.engine
.add_policy_from_file(from_c_str(path)?)
}())
}
/// 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.
#[no_mangle]
pub extern "C" fn regorus_engine_add_data_json(
engine: *mut RegorusEngine,
data: *const c_char,
) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
to_ref(&engine)?
.engine
.add_data(regorus::Value::from_json_str(&from_c_str(data)?)?)
}())
}
#[no_mangle]
pub extern "C" fn regorus_engine_add_data_from_json_file(
engine: *mut RegorusEngine,
path: *const c_char,
) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
to_ref(&engine)?
.engine
.add_data(regorus::Value::from_json_file(&from_c_str(path)?)?)
}())
}
/// Clear policy data.
///
/// See https://docs.rs/regorus/0.1.0-alpha.2/regorus/struct.Engine.html#method.clear_data
#[no_mangle]
pub extern "C" fn regorus_engine_clear_data(engine: *mut RegorusEngine) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
to_ref(&engine)?.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.
#[no_mangle]
pub extern "C" fn regorus_engine_set_input_json(
engine: *mut RegorusEngine,
input: *const c_char,
) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
to_ref(&engine)?
.engine
.set_input(regorus::Value::from_json_str(&from_c_str(input)?)?);
Ok(())
}())
}
#[no_mangle]
pub extern "C" fn regorus_engine_set_input_from_json_file(
engine: *mut RegorusEngine,
path: *const c_char,
) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
to_ref(&engine)?
.engine
.set_input(regorus::Value::from_json_file(&from_c_str(path)?)?);
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.
#[no_mangle]
pub extern "C" fn regorus_engine_eval_query(
engine: *mut RegorusEngine,
query: *const c_char,
) -> RegorusResult {
let output = || -> Result<String> {
let results = to_ref(&engine)?
.engine
.eval_query(from_c_str(query)?, false)?;
Ok(serde_json::to_string_pretty(&results)?)
}();
match output {
Ok(out) => RegorusResult {
status: RegorusStatus::RegorusStatusOk,
output: to_c_str(out),
error_message: std::ptr::null_mut(),
},
Err(e) => to_regorus_result(Err(e)),
}
}

3
bindings/go/go.mod Normal file
View File

@@ -0,0 +1,3 @@
module regorus-test
go 1.21.5

58
bindings/go/main.go Normal file
View File

@@ -0,0 +1,58 @@
package main
import (
"fmt"
"os"
"regorus-test/pkg/regorus"
"time"
)
func main() {
var output string
var err error
t := time.Now();
// Create new engine
engine := regorus.NewEngine()
defer engine.Close()
elapsed1 := time.Since(t)
t = time.Now()
// Add policies and data.
policies := []string{
"../../tests/aci/framework.rego",
"../../tests/aci/api.rego",
"../../tests/aci/policy.rego",
}
for _, policy := range policies {
if err := engine.AddPolicyFromFile(policy); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
}
if err = engine.AddDataFromJsonFile("../../tests/aci/data.json"); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
elapsed2 := time.Since(t)
t = time.Now()
// Set input and eval query.
if err = engine.SetInputFromJsonFile("../../tests/aci/input.json"); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
if output, err = engine.EvalQuery("data.framework.mount_overlay = x"); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
elapsed3 := time.Since(t)
fmt.Println("{%s}", output)
fmt.Printf("NewEngine took %v\n", elapsed1)
fmt.Printf("Add policies and data took %v\n", elapsed2)
fmt.Printf("Set input and eval query took %v\n", elapsed3)
}

View File

@@ -0,0 +1,117 @@
package regorus
// #cgo LDFLAGS: -L ../../../../target/release -lregorus_ffi
// #include "../../../ffi/regorus.h"
import "C"
import (
"fmt"
"unsafe"
)
type Engine struct {
e *C.RegorusEngine
}
func NewEngine() *Engine {
e := new(Engine)
e.e = C.regorus_engine_new()
return e
}
func (e *Engine) Close() {
C.regorus_engine_drop(e.e)
}
func (e *Engine) Clone() *Engine {
c := new(Engine)
c.e = C.regorus_engine_clone(e.e)
return c
}
func (e *Engine) AddPolicy(path string, rego string) error {
path_c := C.CString(path)
defer C.free(unsafe.Pointer(path_c))
rego_c := C.CString(rego)
defer C.free(unsafe.Pointer(rego_c))
result := C.regorus_engine_add_policy(e.e, path_c, rego_c)
defer C.regorus_result_drop(result)
if result.status != C.RegorusStatusOk {
return fmt.Errorf("%s", C.GoString(result.error_message))
}
return nil
}
func (e *Engine) AddPolicyFromFile(path string) error {
path_c := C.CString(path)
defer C.free(unsafe.Pointer(path_c))
result := C.regorus_engine_add_policy_from_file(e.e, path_c)
defer C.regorus_result_drop(result)
if result.status != C.RegorusStatusOk {
return fmt.Errorf("%s", C.GoString(result.error_message))
}
return nil
}
func (e *Engine) AddDataJson(data string) error {
data_c := C.CString(data)
defer C.free(unsafe.Pointer(data_c))
result := C.regorus_engine_add_data_json(e.e, data_c)
defer C.regorus_result_drop(result)
if result.status != C.RegorusStatusOk {
return fmt.Errorf("%s", C.GoString(result.error_message))
}
return nil
}
func (e *Engine) AddDataFromJsonFile(path string) error {
path_c := C.CString(path)
defer C.free(unsafe.Pointer(path_c))
result := C.regorus_engine_add_data_from_json_file(e.e, path_c)
defer C.regorus_result_drop(result)
if result.status != C.RegorusStatusOk {
return fmt.Errorf("%s", C.GoString(result.error_message))
}
return nil
}
func (e *Engine) SetInputJson(input string) error {
input_c := C.CString(input)
defer C.free(unsafe.Pointer(input_c))
result := C.regorus_engine_set_input_json(e.e, input_c)
defer C.regorus_result_drop(result)
if result.status != C.RegorusStatusOk {
return fmt.Errorf("%s", C.GoString(result.error_message))
}
return nil
}
func (e *Engine) SetInputFromJsonFile(path string) error {
path_c := C.CString(path)
defer C.free(unsafe.Pointer(path_c))
result := C.regorus_engine_set_input_from_json_file(e.e, path_c)
defer C.regorus_result_drop(result)
if result.status != C.RegorusStatusOk {
return fmt.Errorf("%s", C.GoString(result.error_message))
}
return nil
}
func (e *Engine) EvalQuery(query string) (string, error) {
query_c := C.CString(query)
defer C.free(unsafe.Pointer(query_c))
result := C.regorus_engine_eval_query(e.e, query_c)
defer C.regorus_result_drop(result)
if result.status != C.RegorusStatusOk {
return "", fmt.Errorf("%s", C.GoString(result.error_message))
}
return C.GoString(result.output), nil
}

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
\\ }
\\ }
\\ ]
\\ }
\\ ]
\\}
```

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

@@ -0,0 +1,146 @@
// 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.
///
/// See https://docs.rs/regorus/0.1.0-alpha.2/regorus/struct.Engine.html#method.clear_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

@@ -16,5 +16,5 @@ if [ -f Cargo.toml ]; then
fi
# Ensure that OPA conformance tests don't regress.
cargo test -r --test opa -- $(tr '\n' ' ' < tests/opa.passing)
cargo test -r --features opa-testutil --test opa -- $(tr '\n' ' ' < tests/opa.passing)
fi

View File

@@ -1,8 +0,0 @@
#!/bin/bash
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
set -e
yaml=$(realpath -e $1)
RUST_BACKTRACE=1 cargo test interpreter::one_yaml -- --include-ignored --nocapture "$yaml"

View File

@@ -1,8 +0,0 @@
#!/bin/bash
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
set -e
yaml=$(realpath -e $1)
RUST_BACKTRACE=1 cargo test parser::one_yaml -- --include-ignored --nocapture "$yaml"

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

@@ -12,8 +12,8 @@ use std::collections::HashMap;
use anyhow::{anyhow, bail, Result};
use chrono::{
DateTime, Datelike, Days, FixedOffset, Local, Months, NaiveDateTime, SecondsFormat, TimeZone,
Timelike, Utc, Weekday,
DateTime, Datelike, Days, FixedOffset, Local, Months, SecondsFormat, TimeZone, Timelike, Utc,
Weekday,
};
use chrono_tz::Tz;
@@ -124,7 +124,7 @@ fn format(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) -> R
let (datetime, format) = parse_epoch(name, &params[0], &args[0])?;
let result = match format {
Some(format) => datetime.format(&format).to_string(),
Some(format) => compat::format(datetime, layout_with_predefined_formats(&format)),
None => datetime.to_rfc3339_opts(SecondsFormat::AutoSi, true),
};
@@ -159,7 +159,7 @@ fn parse_ns(span: &Span, params: &[Ref<Expr>], args: &[Value], strict: bool) ->
let layout = ensure_string(name, &params[0], &args[0])?;
let value = ensure_string(name, &params[1], &args[1])?;
let datetime = NaiveDateTime::parse_from_str(&value, &layout)?;
let datetime = compat::parse(layout_with_predefined_formats(&layout), &value)?;
safe_timestamp_nanos(span, strict, datetime.timestamp_nanos_opt())
}
@@ -275,3 +275,21 @@ fn parse_epoch(
"`{fcn}` expects `ns` to be a `number` or `array[number, string]`. Got `{val}` instead"
)))
}
fn layout_with_predefined_formats(format: &str) -> &str {
match format {
"ANSIC" => "Mon Jan _2 15:04:05 2006",
"UnixDate" => "Mon Jan _2 15:04:05 MST 2006",
"RubyDate" => "Mon Jan 02 15:04:05 -0700 2006",
"RFC822" => "02 Jan 06 15:04 MST",
// RFC822 with numeric zone
"RFC822Z" => "02 Jan 06 15:04 -0700",
"RFC850" => "Monday, 02-Jan-06 15:04:05 MST",
"RFC1123" => "Mon, 02 Jan 2006 15:04:05 MST",
// RFC1123 with numeric zone
"RFC1123Z" => "Mon, 02 Jan 2006 15:04:05 -0700",
"RFC3339" => "2006-01-02T15:04:05Z07:00",
"RFC3339Nano" => "2006-01-02T15:04:05.999999999Z07:00",
other => other,
}
}

View File

@@ -33,8 +33,13 @@
use std::error::Error;
use std::fmt;
use std::iter;
use chrono::Duration;
use chrono::TimeZone;
use chrono::{
format::{self, Fixed, Parsed},
DateTime, Duration, FixedOffset, ParseResult,
};
const NANOSECOND: u64 = 1;
const MICROSECOND: u64 = 1000 * NANOSECOND;
@@ -250,8 +255,340 @@ fn leading_fraction(s: &str) -> (u64, f64, &str) {
(num, scale, &s[last_idx..])
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum GoTimeFormatItemsMode {
Parse,
Format,
}
#[derive(Debug, Clone)]
struct GoTimeFormatItems<'a> {
reminder: &'a str,
queue: &'static [format::Item<'static>],
mode: GoTimeFormatItemsMode,
}
impl<'a> GoTimeFormatItems<'a> {
fn parse(reminder: &str) -> GoTimeFormatItems {
GoTimeFormatItems {
reminder,
queue: &[],
mode: GoTimeFormatItemsMode::Parse,
}
}
fn format(reminder: &str) -> GoTimeFormatItems {
GoTimeFormatItems {
reminder,
queue: &[],
mode: GoTimeFormatItemsMode::Format,
}
}
}
impl<'a> Iterator for GoTimeFormatItems<'a> {
type Item = format::Item<'a>;
fn next(&mut self) -> Option<Self::Item> {
use format::{Fixed::*, Item::*, Numeric, Pad};
macro_rules! token {
($prefix:expr, $kind:expr $(, $queue:expr)*) => {
if self.reminder.starts_with($prefix) {
self.reminder = &self.reminder[$prefix.len()..];
self.queue = &[$($queue),*];
return Some($kind);
}
};
}
fn is_fractional_seconds(val: &str) -> bool {
// first char is either '.' or ','
let mut chars = val.chars().skip(1);
let Some(repeating @ ('0' | '9')) = chars.next() else {
return false;
};
let next = chars.find(|c| c != &repeating);
!matches!(next, Some('0'..='9'))
}
if let Some((item, reminder)) = self.queue.split_first() {
self.queue = reminder;
return Some(item.clone());
}
match self.reminder.chars().next() {
// January, Jan
Some('J') => {
token!("January", Fixed(LongMonthName));
token!("Jan", Fixed(ShortMonthName));
}
// Monday, Mon, MST
Some('M') => {
token!("Monday", Fixed(LongWeekdayName));
token!("Mon", Fixed(ShortWeekdayName));
token!("MST", Fixed(TimezoneName));
}
// 01, 02, 03, 04, 05, 06, 002
Some('0') => {
token!("002", Numeric(Numeric::Ordinal, Pad::Zero));
token!("01", Numeric(Numeric::Month, Pad::Zero));
token!("02", Numeric(Numeric::Day, Pad::Zero));
token!("03", Numeric(Numeric::Hour12, Pad::Zero));
token!("04", Numeric(Numeric::Minute, Pad::Zero));
if self.reminder.starts_with("05") {
self.reminder = &self.reminder[2..];
if !self.reminder.starts_with('.') && self.mode == GoTimeFormatItemsMode::Parse
{
self.queue = &[Fixed(Nanosecond)];
}
return Some(Numeric(Numeric::Second, Pad::Zero));
}
token!("06", Numeric(Numeric::YearMod100, Pad::Zero));
}
// 15, 1
Some('1') => {
use Numeric::*;
token!("15", Numeric(Hour, Pad::Zero));
token!("1", Numeric(Month, Pad::None));
}
// 2006, 2
Some('2') => {
use Numeric::*;
token!("2006", Numeric(Year, Pad::Zero));
token!("2", Numeric(Day, Pad::None));
}
// _2, _2006, __2
Some('_') => {
use Numeric::*;
token!("_2006", Literal("_"), Numeric(Year, Pad::None));
token!("__2", Numeric(Ordinal, Pad::Space));
token!("_2", Numeric(Day, Pad::Space));
}
Some('3') => {
use Numeric::*;
token!("3", Numeric(Hour12, Pad::None));
}
Some('4') => {
use Numeric::*;
token!("4", Numeric(Minute, Pad::None));
}
Some('5') => {
token!("5", Numeric(Numeric::Second, Pad::None), Fixed(Nanosecond));
}
// PM
Some('P') => {
token!("PM", Fixed(UpperAmPm));
}
// pm
Some('p') => {
token!("pm", Fixed(LowerAmPm));
}
// -070000, -07:00:00, -0700, -07:00, -07
Some('-') => {
token!("-070000", Fixed(TimezoneOffsetDoubleColon));
token!("-07:00:00", Fixed(TimezoneOffsetDoubleColon));
token!("-0700", Fixed(TimezoneOffset));
token!("-07:00", Fixed(TimezoneOffsetColon));
token!("-07", Fixed(TimezoneOffsetTripleColon));
token!("-", Literal("-"));
}
// Z070000, Z07:00:00, Z0700, Z07:00, Z07
Some('Z') => {
// token!("Z070000", Fixed(TimezoneOffsetDoubleColonZ));
// token!("Z07:00:00", Fixed(TimezoneOffsetDoubleColonZ));
token!("Z0700", Fixed(TimezoneOffsetZ));
token!("Z07:00", Fixed(TimezoneOffsetColonZ));
// token!("Z07", Fixed(TimezoneOffsetTripleColonZ));
}
// ,000, or .000, or ,999, or .999 - repeated digits for fractional seconds.
Some('.' | ',') if is_fractional_seconds(self.reminder) => {
token!(".000000000", Fixed(Nanosecond9));
token!(".00000000", Fixed(Nanosecond));
token!(".0000000", Fixed(Nanosecond));
token!(".000000", Fixed(Nanosecond6));
token!(".00000", Fixed(Nanosecond));
token!(".0000", Fixed(Nanosecond));
token!(".000", Fixed(Nanosecond3));
token!(".00", Fixed(Nanosecond));
token!(".0", Fixed(Nanosecond));
token!(".999999999", Fixed(Nanosecond));
token!(".99999999", Fixed(Nanosecond));
token!(".9999999", Fixed(Nanosecond));
token!(".999999", Fixed(Nanosecond));
token!(".99999", Fixed(Nanosecond));
token!(".9999", Fixed(Nanosecond));
token!(".999", Fixed(Nanosecond));
token!(".99", Fixed(Nanosecond));
token!(".9", Fixed(Nanosecond));
token!(".", Literal("."));
token!(",000000000", Fixed(Nanosecond9));
token!(",00000000", Fixed(Nanosecond));
token!(",0000000", Fixed(Nanosecond));
token!(",000000", Fixed(Nanosecond6));
token!(",00000", Fixed(Nanosecond));
token!(",0000", Fixed(Nanosecond));
token!(",000", Fixed(Nanosecond3));
token!(",00", Fixed(Nanosecond));
token!(",0", Fixed(Nanosecond));
token!(",999999999", Fixed(Nanosecond9));
token!(",99999999", Fixed(Nanosecond));
token!(",9999999", Fixed(Nanosecond));
token!(",999999", Fixed(Nanosecond6));
token!(",99999", Fixed(Nanosecond));
token!(",9999", Fixed(Nanosecond));
token!(",999", Fixed(Nanosecond3));
token!(",99", Fixed(Nanosecond));
token!(",9", Fixed(Nanosecond));
token!(",", Literal(","));
}
Some(c) if c.is_whitespace() => {
let next_non_ws = self
.reminder
.find(|c: char| !c.is_whitespace())
.unwrap_or(self.reminder.len());
let literal = &self.reminder[..next_non_ws];
token!(&literal, Space(literal));
}
Some(_) => {
let literal = &self.reminder[..1];
token!(&literal, Literal(literal));
}
None => {}
}
None
}
}
// Adapted from chrono's `scan::timezone_offset_2822`:
// https://github.com/chronotope/chrono/blob/baa55d084784e4e88b5332efe8e96af794a52e8a/src/format/scan.rs#L285-L322
fn parse_legacy_timezone(parsed: &mut Parsed, val: &str) -> ParseResult<()> {
let upto = val
.as_bytes()
.iter()
.position(|&c| !c.is_ascii_alphabetic())
.unwrap_or(val.len());
if upto == 0 {
return Ok(());
}
let name = &val.as_bytes()[..upto];
if name.eq_ignore_ascii_case(b"gmt") || name.eq_ignore_ascii_case(b"ut") {
parsed.set_offset(0)
} else if name.eq_ignore_ascii_case(b"edt") {
parsed.set_offset(-4 * 3600)
} else if name.eq_ignore_ascii_case(b"est") || name.eq_ignore_ascii_case(b"cdt") {
parsed.set_offset(-5 * 3600)
} else if name.eq_ignore_ascii_case(b"cst") || name.eq_ignore_ascii_case(b"mdt") {
parsed.set_offset(-6 * 3600)
} else if name.eq_ignore_ascii_case(b"mst") || name.eq_ignore_ascii_case(b"pdt") {
parsed.set_offset(-7 * 3600)
} else if name.eq_ignore_ascii_case(b"pst") {
parsed.set_offset(-8 * 3600)
} else {
Ok(())
}
}
// Parses a date in Go's time format like 'Mon Jan _2 15:04:05 2006'.
pub fn parse(layout: &str, value: &str) -> ParseResult<DateTime<FixedOffset>> {
let mut items = GoTimeFormatItems::parse(layout);
let mut parsed = Parsed::new();
let remainder = format::parse_and_remainder(
&mut parsed,
value,
items
.by_ref()
.take_while(|i| !matches!(i, format::Item::Fixed(Fixed::TimezoneName))),
)?;
// The reason for splitting parsing procedure to two part is handling legacy
// time zone names like EDT, EST etc. They are supported by chrono but not
// exposed to us. As a workaround, we copied chrono's implementation to
// `parse_legacy_timezone` function and whenever we encounter a
// `Fixed::TimezoneName` we stop parsing with our regular parser,
// parse timezone with `parse_legacy_timezone` and then continue parsing.
if !remainder.is_empty() {
parse_legacy_timezone(&mut parsed, remainder)?;
format::parse(
&mut parsed,
remainder,
iter::once(format::Item::Fixed(Fixed::TimezoneName)).chain(items),
)?;
}
// Go's `time.Parse` allows missing years but chrono fails to parse them,
// we're setting year field to `0` if year field is missing.
if parsed.year.is_none()
&& parsed.year_div_100.is_none()
&& parsed.year_mod_100.is_none()
&& parsed.isoyear.is_none()
&& parsed.isoyear_div_100.is_none()
&& parsed.isoyear_mod_100.is_none()
&& parsed.timestamp.is_none()
{
parsed.set_year(0)?;
}
// Go's `time.Parse` allows missing time (hour, minute, second) but
// chrono fails to parse them, we're setting time to `0` if time is missing.
if parsed.hour_div_12.is_none()
&& parsed.hour_mod_12.is_none()
&& parsed.minute.is_none()
&& parsed.second.is_none()
{
parsed.set_hour(0)?;
parsed.set_minute(0)?;
parsed.set_second(0)?;
}
if parsed.offset.is_some() {
parsed.to_datetime()
} else {
let naive = parsed.to_naive_datetime_with_offset(0)?;
Ok(naive.and_utc().fixed_offset())
}
}
// Formats a date in Go's time format like 'Mon Jan _2 15:04:05 2006'.
pub fn format<Tz: TimeZone>(date: DateTime<Tz>, fmt: &str) -> String
where
Tz::Offset: fmt::Display,
{
date.format_with_items(GoTimeFormatItems::format(fmt))
.to_string()
}
#[cfg(test)]
mod tests {
use chrono::{Datelike, Month, TimeZone, Timelike, Weekday};
use chrono_tz::PST8PDT;
use super::*;
#[test]
@@ -353,4 +690,640 @@ mod tests {
assert_eq!(dur, expected_dur);
}
}
#[test]
fn parses_datetimes() {
// Test cases are copied from Go's `time.Parse` tests:
// https://github.com/golang/go/blob/e9b3ff15f40d6b258217b3467c662f816b078477/src/time/format_test.go#L266-L339
struct ParseTest {
name: String,
format: String,
value: String,
has_tz: bool, // contains a time zone
has_wd: bool, // contains a weekday
year_sign: i32, // sign of year, -1 indicates the year is not present in the format
frac_digits: usize, // number of digits of fractional second
}
fn parse_test_case(
name: &str,
format: &str,
value: &str,
has_tz: bool,
has_wd: bool,
year_sign: i32,
frac_digits: usize,
) -> ParseTest {
ParseTest {
name: name.to_string(),
format: format.to_string(),
value: value.to_string(),
has_tz,
has_wd,
year_sign,
frac_digits,
}
}
fn check_time(time: DateTime<FixedOffset>, test_case: &ParseTest) {
// The time should be Thu Feb 4 21:00:57 PST 2010
if test_case.year_sign >= 0 {
assert_eq!(test_case.year_sign * time.year(), 2010);
}
assert_eq!(time.month0(), Month::February as u32);
assert_eq!(time.day(), 4);
assert_eq!(time.hour(), 21);
assert_eq!(time.minute(), 0);
assert_eq!(time.second(), 57);
let nanosec = "012345678"[..test_case.frac_digits].to_string()
+ &"000000000"[..9 - test_case.frac_digits];
assert_eq!(time.nanosecond(), nanosec.parse::<u32>().unwrap());
if test_case.has_tz {
assert_eq!(time.timezone().local_minus_utc(), -28800);
}
if test_case.has_wd {
assert_eq!(time.weekday(), Weekday::Thu);
}
}
let test_cases = vec![
parse_test_case(
"ANSIC",
ANSIC,
"Thu Feb 4 21:00:57 2010",
false,
true,
1,
0,
),
parse_test_case(
"UnixDate",
UNIX_DATE,
"Thu Feb 4 21:00:57 PST 2010",
true,
true,
1,
0,
),
parse_test_case(
"RubyDate",
RUBY_DATE,
"Thu Feb 04 21:00:57 -0800 2010",
true,
true,
1,
0,
),
parse_test_case(
"RFC850",
RFC850,
"Thursday, 04-Feb-10 21:00:57 PST",
true,
true,
1,
0,
),
parse_test_case(
"RFC1123",
RFC1123,
"Thu, 04 Feb 2010 21:00:57 PST",
true,
true,
1,
0,
),
// parse_test_case(
// "RFC1123",
// RFC1123,
// "Thu, 04 Feb 2010 22:00:57 PDT",
// true,
// true,
// 1,
// 0,
// ),
parse_test_case(
"RFC1123Z",
RFC1123Z,
"Thu, 04 Feb 2010 21:00:57 -0800",
true,
true,
1,
0,
),
parse_test_case(
"RFC3339",
RFC3339,
"2010-02-04T21:00:57-08:00",
true,
false,
1,
0,
),
// parse_test_case(
// "custom: \"2006-01-02 15:04:05-07\"",
// "2006-01-02 15:04:05-07",
// "2010-02-04 21:00:57-08",
// true,
// false,
// 1,
// 0,
// ),
// Optional fractional seconds.
parse_test_case(
"ANSIC",
ANSIC,
"Thu Feb 4 21:00:57.0 2010",
false,
true,
1,
1,
),
parse_test_case(
"UnixDate",
UNIX_DATE,
"Thu Feb 4 21:00:57.01 PST 2010",
true,
true,
1,
2,
),
parse_test_case(
"RubyDate",
RUBY_DATE,
"Thu Feb 04 21:00:57.012 -0800 2010",
true,
true,
1,
3,
),
parse_test_case(
"RFC850",
RFC850,
"Thursday, 04-Feb-10 21:00:57.0123 PST",
true,
true,
1,
4,
),
parse_test_case(
"RFC1123",
RFC1123,
"Thu, 04 Feb 2010 21:00:57.01234 PST",
true,
true,
1,
5,
),
parse_test_case(
"RFC1123Z",
RFC1123Z,
"Thu, 04 Feb 2010 21:00:57.01234 -0800",
true,
true,
1,
5,
),
parse_test_case(
"RFC3339",
RFC3339,
"2010-02-04T21:00:57.012345678-08:00",
true,
false,
1,
9,
),
parse_test_case(
"custom: \"2006-01-02 15:04:05\"",
"2006-01-02 15:04:05",
"2010-02-04 21:00:57.0",
false,
false,
1,
0,
),
// Amount of white space should not matter.
parse_test_case("ANSIC", ANSIC, "Thu Feb 4 21:00:57 2010", false, true, 1, 0),
parse_test_case(
"ANSIC",
ANSIC,
"Thu Feb 4 21:00:57 2010",
false,
true,
1,
0,
),
// Case should not matter
parse_test_case("ANSIC", ANSIC, "THU FEB 4 21:00:57 2010", false, true, 1, 0),
parse_test_case("ANSIC", ANSIC, "thu feb 4 21:00:57 2010", false, true, 1, 0),
// Fractional seconds.
parse_test_case(
"millisecond:: dot separator",
"Mon Jan _2 15:04:05.000 2006",
"Thu Feb 4 21:00:57.012 2010",
false,
true,
1,
3,
),
parse_test_case(
"microsecond:: dot separator",
"Mon Jan _2 15:04:05.000000 2006",
"Thu Feb 4 21:00:57.012345 2010",
false,
true,
1,
6,
),
parse_test_case(
"nanosecond:: dot separator",
"Mon Jan _2 15:04:05.000000000 2006",
"Thu Feb 4 21:00:57.012345678 2010",
false,
true,
1,
9,
),
parse_test_case(
"millisecond:: comma separator",
"Mon Jan _2 15:04:05,000 2006",
"Thu Feb 4 21:00:57.012 2010",
false,
true,
1,
3,
),
parse_test_case(
"microsecond:: comma separator",
"Mon Jan _2 15:04:05,000000 2006",
"Thu Feb 4 21:00:57.012345 2010",
false,
true,
1,
6,
),
parse_test_case(
"nanosecond:: comma separator",
"Mon Jan _2 15:04:05,000000000 2006",
"Thu Feb 4 21:00:57.012345678 2010",
false,
true,
1,
9,
),
// Leading zeros in other places should not be taken as fractional seconds.
parse_test_case(
"zero1",
"2006.01.02.15.04.05.0",
"2010.02.04.21.00.57.0",
false,
false,
1,
1,
),
parse_test_case(
"zero2",
"2006.01.02.15.04.05.00",
"2010.02.04.21.00.57.01",
false,
false,
1,
2,
),
// Month and day names only match when not followed by a lower-case letter.
// parse_test_case(
// "Janet",
// "Hi Janet, the Month is January: Jan _2 15:04:05 2006",
// "Hi Janet, the Month is February: Feb 4 21:00:57 2010",
// false,
// true,
// 1,
// 0,
// ),
// GMT with offset.
// parse_test_case(
// "GMT-8",
// UNIX_DATE,
// "Fri Feb 5 05:00:57 GMT-8 2010",
// true,
// true,
// 1,
// 0,
// ),
// Accept any number of fractional second digits (including none) for .999...
// In Go 1, .999... was completely ignored in the format, meaning the first two
// cases would succeed, but the next four would not. Go 1.1 accepts all six.
// decimal "." separator.
parse_test_case(
"",
"2006-01-02 15:04:05.9999 -0700 MST",
"2010-02-04 21:00:57 -0800 PST",
true,
false,
1,
0,
),
parse_test_case(
"",
"2006-01-02 15:04:05.999999999 -0700 MST",
"2010-02-04 21:00:57 -0800 PST",
true,
false,
1,
0,
),
parse_test_case(
"",
"2006-01-02 15:04:05.9999 -0700 MST",
"2010-02-04 21:00:57.0123 -0800 PST",
true,
false,
1,
4,
),
parse_test_case(
"",
"2006-01-02 15:04:05.999999999 -0700 MST",
"2010-02-04 21:00:57.0123 -0800 PST",
true,
false,
1,
4,
),
parse_test_case(
"",
"2006-01-02 15:04:05.9999 -0700 MST",
"2010-02-04 21:00:57.012345678 -0800 PST",
true,
false,
1,
9,
),
parse_test_case(
"",
"2006-01-02 15:04:05.999999999 -0700 MST",
"2010-02-04 21:00:57.012345678 -0800 PST",
true,
false,
1,
9,
),
// comma "," separator.
parse_test_case(
"",
"2006-01-02 15:04:05,9999 -0700 MST",
"2010-02-04 21:00:57 -0800 PST",
true,
false,
1,
0,
),
parse_test_case(
"",
"2006-01-02 15:04:05,999999999 -0700 MST",
"2010-02-04 21:00:57 -0800 PST",
true,
false,
1,
0,
),
parse_test_case(
"",
"2006-01-02 15:04:05,9999 -0700 MST",
"2010-02-04 21:00:57.0123 -0800 PST",
true,
false,
1,
4,
),
parse_test_case(
"",
"2006-01-02 15:04:05,999999999 -0700 MST",
"2010-02-04 21:00:57.0123 -0800 PST",
true,
false,
1,
4,
),
parse_test_case(
"",
"2006-01-02 15:04:05,9999 -0700 MST",
"2010-02-04 21:00:57.012345678 -0800 PST",
true,
false,
1,
9,
),
parse_test_case(
"",
"2006-01-02 15:04:05,999999999 -0700 MST",
"2010-02-04 21:00:57.012345678 -0800 PST",
true,
false,
1,
9,
),
// issue 4502.
parse_test_case(
"",
STAMP_NANO,
"Feb 4 21:00:57.012345678",
false,
false,
-1,
9,
),
parse_test_case(
"",
"Jan _2 15:04:05.999",
"Feb 4 21:00:57.012300000",
false,
false,
-1,
4,
),
parse_test_case(
"",
"Jan _2 15:04:05.999",
"Feb 4 21:00:57.012345678",
false,
false,
-1,
9,
),
parse_test_case(
"",
"Jan _2 15:04:05.999999999",
"Feb 4 21:00:57.0123",
false,
false,
-1,
4,
),
parse_test_case(
"",
"Jan _2 15:04:05.999999999",
"Feb 4 21:00:57.012345678",
false,
false,
-1,
9,
),
// Day of year.
parse_test_case(
"",
"2006-01-02 002 15:04:05",
"2010-02-04 035 21:00:57",
false,
false,
1,
0,
),
parse_test_case(
"",
"2006-01 002 15:04:05",
"2010-02 035 21:00:57",
false,
false,
1,
0,
),
parse_test_case(
"",
"2006-002 15:04:05",
"2010-035 21:00:57",
false,
false,
1,
0,
),
parse_test_case(
"",
"200600201 15:04:05",
"201003502 21:00:57",
false,
false,
1,
0,
),
// parse_test_case(
// "",
// "200600204 15:04:05",
// "201003504 21:00:57",
// false,
// false,
// 1,
// 0,
// ),
];
for tc in test_cases {
println!("Test case {}", tc.name);
let time = parse(&tc.format, &tc.value).unwrap();
check_time(time, &tc);
}
}
#[test]
fn formats_datetimes() {
// Test cases are copied from Go's `time.Format` tests:
// https://github.com/golang/go/blob/e9b3ff15f40d6b258217b3467c662f816b078477/src/time/format_test.go#L144-L176
struct FormatTest {
name: String,
format: String,
result: String,
}
fn format_test_case(name: &str, format: &str, result: &str) -> FormatTest {
FormatTest {
name: name.to_string(),
format: format.to_string(),
result: result.to_string(),
}
}
let test_cases = vec![
format_test_case("ANSIC", ANSIC, "Wed Feb 4 21:00:57 2009"),
format_test_case("UnixDate", UNIX_DATE, "Wed Feb 4 21:00:57 PST 2009"),
format_test_case("RubyDate", RUBY_DATE, "Wed Feb 04 21:00:57 -0800 2009"),
format_test_case("RFC822", RFC822, "04 Feb 09 21:00 PST"),
format_test_case("RFC850", RFC850, "Wednesday, 04-Feb-09 21:00:57 PST"),
format_test_case("RFC1123", RFC1123, "Wed, 04 Feb 2009 21:00:57 PST"),
format_test_case("RFC1123Z", RFC1123Z, "Wed, 04 Feb 2009 21:00:57 -0800"),
format_test_case("RFC3339", RFC3339, "2009-02-04T21:00:57-08:00"),
// format_test_case(
// "RFC3339Nano",
// RFC3339_NANO,
// "2009-02-04T21:00:57.0123456-08:00",
// ),
format_test_case("Kitchen", KITCHEN, "9:00PM"),
format_test_case("am/pm", "3pm", "9pm"),
format_test_case("AM/PM", "3PM", "9PM"),
format_test_case("two-digit year", "06 01 02", "09 02 04"),
// Three-letter months and days must not be followed by lower-case letter.
// format_test_case(
// "Janet",
// "Hi Janet, the Month is January",
// "Hi Janet, the Month is February",
// ),
// Time stamps, Fractional seconds.
format_test_case("Stamp", STAMP, "Feb 4 21:00:57"),
format_test_case("StampMilli", STAMP_MILLI, "Feb 4 21:00:57.012"),
format_test_case("StampMicro", STAMP_MICRO, "Feb 4 21:00:57.012345"),
format_test_case("StampNano", STAMP_NANO, "Feb 4 21:00:57.012345600"),
format_test_case("DateTime", DATE_TIME, "2009-02-04 21:00:57"),
format_test_case("DateOnly", DATE_ONLY, "2009-02-04"),
format_test_case("TimeOnly", TIME_ONLY, "21:00:57"),
format_test_case("YearDay", "Jan 2 002 __2 2", "Feb 4 035 35 4"),
// format_test_case("Year", "2006 6 06 _6 __6 ___6", "2009 6 09 _6 __6 ___6"),
// format_test_case("Month", "Jan January 1 01 _1", "Feb February 2 02 _2"),
format_test_case("DayOfMonth", "2 02 _2 __2", "4 04 4 35"),
format_test_case("DayOfWeek", "Mon Monday", "Wed Wednesday"),
// format_test_case("Hour", "15 3 03 _3", "21 9 09 _9"),
// format_test_case("Minute", "4 04 _4", "0 00 _0"),
// format_test_case("Second", "5 05 _5", "57 57 _57"),
];
// The numeric time represents Thu Feb 4 21:00:57.012345600 PST 2009
let time = PST8PDT.timestamp_nanos(1233810057012345600);
for tc in test_cases {
println!("Test case {}", tc.name);
let result = format(time, &tc.format);
assert_eq!(result, tc.result);
}
}
#[test]
fn parses_date_only() {
let time = parse("2006-01-02", "2020-02-02").unwrap();
assert_eq!(time.year(), 2020);
assert_eq!(time.month(), 2);
assert_eq!(time.day(), 2);
}
const _LAYOUT: &str = "01/02 03:04:05PM '06 -0700"; // The reference time, in numerical order.
const ANSIC: &str = "Mon Jan _2 15:04:05 2006";
const UNIX_DATE: &str = "Mon Jan _2 15:04:05 MST 2006";
const RUBY_DATE: &str = "Mon Jan 02 15:04:05 -0700 2006";
const RFC822: &str = "02 Jan 06 15:04 MST";
const _RFC822Z: &str = "02 Jan 06 15:04 -0700"; // RFC822 with numeric zone
const RFC850: &str = "Monday, 02-Jan-06 15:04:05 MST";
const RFC1123: &str = "Mon, 02 Jan 2006 15:04:05 MST";
const RFC1123Z: &str = "Mon, 02 Jan 2006 15:04:05 -0700"; // RFC1123 with numeric zone
const RFC3339: &str = "2006-01-02T15:04:05Z07:00";
const _RFC3339_NANO: &str = "2006-01-02T15:04:05.999999999Z07:00";
const KITCHEN: &str = "3:04PM";
// Handy time stamps.
const STAMP: &str = "Jan _2 15:04:05";
const STAMP_MILLI: &str = "Jan _2 15:04:05.000";
const STAMP_MICRO: &str = "Jan _2 15:04:05.000000";
const STAMP_NANO: &str = "Jan _2 15:04:05.000000000";
const DATE_TIME: &str = "2006-01-02 15:04:05";
const DATE_ONLY: &str = "2006-01-02";
const TIME_ONLY: &str = "15:04:05";
}

View File

@@ -2,7 +2,7 @@
// Licensed under the MIT and Apache 2.0 License.
use anyhow::{anyhow, Result};
use chrono::{DateTime, Datelike, FixedOffset, TimeZone, Timelike, Utc};
use chrono::{DateTime, Datelike, FixedOffset, NaiveDate, Timelike};
// Adapted from the official Go implementation:
// https://github.com/open-policy-agent/opa/blob/eb17a716b97720a27c6569395ba7c4b7409aae87/topdown/time.go#L179-L243
@@ -12,7 +12,6 @@ pub fn diff_between_datetimes(
) -> Result<(i32, i32, i32, i32, i32, i32)> {
// The following implementation of this function is taken
// from https://github.com/icza/gox licensed under Apache 2.0.
// The only modification made is to variable names.
//
// For details, see https://stackoverflow.com/a/36531443/1705598
//
@@ -50,12 +49,9 @@ pub fn diff_between_datetimes(
day -= 1;
}
if day < 0 {
// Days in month:
let t = Utc
.with_ymd_and_hms(datetime1.year(), datetime1.month(), 32, 0, 0, 0)
.single()
let days_in_month = days_in_month(datetime1.year(), datetime1.month())
.ok_or(anyhow!("Could not convert `ns1` to datetime"))?;
day += 32 - t.day() as i32;
day += days_in_month as i32;
month -= 1;
}
if month < 0 {
@@ -67,3 +63,21 @@ pub fn diff_between_datetimes(
Ok((year, month, day, hour, min, sec))
}
fn days_in_month(year: i32, month: u32) -> Option<i64> {
Some(
NaiveDate::from_ymd_opt(
match month {
12 => year + 1,
_ => year,
},
match month {
12 => 1,
_ => month + 1,
},
1,
)?
.signed_duration_since(NaiveDate::from_ymd_opt(year, month, 1)?)
.num_days(),
)
}

View File

@@ -8,12 +8,12 @@ use crate::parser::*;
use crate::scheduler::*;
use crate::utils::gather_functions;
use crate::value::*;
use crate::QueryResults;
use crate::{Extension, 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)
}
@@ -345,4 +348,92 @@ impl Engine {
self.interpreter.create_rule_prefixes()?;
Ok(self.interpreter.get_data_mut().clone())
}
/// Add a custom builtin (extension).
///
/// * `path`: The fully qualified path of the builtin.
/// * `nargs`: The number of arguments the builtin takes.
/// * `extension`: The [`Extension`] instance.
///
/// ```rust
/// # use regorus::*;
/// # use anyhow::{bail, Result};
/// # fn main() -> Result<()> {
/// let mut engine = Engine::new();
///
/// // Policy uses `do_magic` custom builtin.
/// engine.add_policy(
/// "test.rego".to_string(),
/// r#"package test
/// x = do_magic(1)
/// "#.to_string(),
/// )?;
///
/// // Evaluating fails since `do_magic` is not defined.
/// assert!(engine.eval_query("data.test.x".to_string(), false).is_err());
///
/// // Add extension to implement `do_magic`. The extension can be stateful.
/// let mut magic = 8;
/// engine.add_extension("do_magic".to_string(), 1 , Box::new(move | mut params: Vec<Value> | {
/// // params is mut and therefore individual values can be removed from it and modified.
/// // The number of parameters (1) has already been validated.
///
/// match &params[0].as_i64() {
/// Ok(i) => {
/// // Compute value
/// let v = *i + magic;
/// // Update extension state.
/// magic += 1;
/// Ok(Value::from(v))
/// }
/// // Extensions can raise errors. Regorus will add location information to
/// // the error.
/// _ => bail!("do_magic expects i64 value")
/// }
/// }))?;
///
/// // Evaluation will now succeed.
/// let r = engine.eval_query("data.test.x".to_string(), false)?;
/// assert_eq!(r.result[0].expressions[0].value.as_i64()?, 9);
///
/// // Cloning the engine will also clone the extension.
/// let mut engine1 = engine.clone();
///
/// // Evaluating again will return a different value since the extension is stateful.
/// let r = engine.eval_query("data.test.x".to_string(), false)?;
/// assert_eq!(r.result[0].expressions[0].value.as_i64()?, 10);
///
/// // The second engine has a clone of the extension.
/// let r = engine1.eval_query("data.test.x".to_string(), false)?;
/// assert_eq!(r.result[0].expressions[0].value.as_i64()?, 10);
///
/// // Once added, the extension cannot be replaced or removed.
/// assert!(engine.add_extension("do_magic".to_string(), 1, Box::new(|_:Vec<Value>| {
/// Ok(Value::Undefined)
/// })).is_err());
///
/// // Extensions don't support out-parameter syntax.
/// engine.add_policy(
/// "policy.rego".to_string(),
/// r#"package invalid
/// x = y {
/// # y = do_magic(2)
/// do_magic(2, y) # y is supplied as an out parameter.
/// }
/// "#.to_string()
/// )?;
///
/// // Evaluation fails since y is not defined.
/// assert!(engine.eval_query("data.invalid.y".to_string(), false).is_err());
/// # Ok(())
/// # }
/// ```
pub fn add_extension(
&mut self,
path: String,
nargs: u8,
extension: Box<dyn Extension>,
) -> Result<()> {
self.interpreter.add_extension(path, nargs, extension)
}
}

View File

@@ -9,7 +9,7 @@ use crate::parser::Parser;
use crate::scheduler::*;
use crate::utils::*;
use crate::value::*;
use crate::{Expression, Location, QueryResult, QueryResults};
use crate::{Expression, Extension, Location, QueryResult, QueryResults};
use anyhow::{anyhow, bail, Result};
use log::info;
@@ -65,6 +65,7 @@ pub struct Interpreter {
allow_deprecated: bool,
strict_builtin_errors: bool,
imports: BTreeMap<String, Ref<Expr>>,
extensions: HashMap<String, (u8, Box<dyn Extension>)>,
}
impl Default for Interpreter {
@@ -175,6 +176,7 @@ impl Interpreter {
allow_deprecated: true,
strict_builtin_errors: true,
imports: BTreeMap::default(),
extensions: HashMap::new(),
}
}
@@ -1998,14 +2000,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 +2060,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));
@@ -2100,6 +2103,7 @@ impl Interpreter {
}
let orig_fcn_path = fcn_path;
let mut with_functions_saved = None;
let fcn_path = match self.with_functions.get(&orig_fcn_path) {
Some(FunctionModifier::Function(p)) => {
@@ -2120,9 +2124,10 @@ impl Interpreter {
_ => orig_fcn_path.clone(),
};
let empty = vec![];
let fcns_rules = match self.lookup_function_by_name(&fcn_path) {
Some(r) => r,
let mut extension = None;
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 +2136,15 @@ impl Interpreter {
.is_some()
{
// process default functions later.
&empty
(&empty, self.module.clone())
}
// Look up extension.
else if let Some(ext) = self.extensions.get_mut(&fcn_path) {
extension = Some(ext);
(&empty, None)
}
// 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;
@@ -2152,6 +2162,21 @@ impl Interpreter {
return Ok(Value::Undefined);
}
if let Some((nargs, ext)) = extension {
if param_values.len() != *nargs as usize {
bail!(span.error("incorrect number of parameters supplied to extension"));
}
let r = ext(param_values);
// Restore with_functions.
if let Some(with_functions) = with_functions_saved {
self.with_functions = with_functions;
}
match r {
Ok(v) => return Ok(v),
Err(e) => bail!(span.error(&format!("{e}"))),
}
}
let fcns = fcns_rules.clone();
let mut results: Vec<Value> = Vec::new();
@@ -2213,6 +2238,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 +2248,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(),
@@ -3013,43 +3040,7 @@ impl Interpreter {
Ok(())
}
pub fn eval_rule(&mut self, module: &Ref<Module>, rule: &Ref<Rule>) -> Result<()> {
// Skip reprocessing rule
if self.processed.contains(rule) {
return Ok(());
}
// Skip default rules
if let Rule::Default { .. } = rule.as_ref() {
return Ok(());
}
self.active_rules.push(rule.clone());
if self.active_rules.iter().filter(|&r| r == rule).count() == 2 {
let mut msg = String::default();
for r in &self.active_rules {
let refr = Self::get_rule_refr(r);
let span = refr.span();
msg += span
.source
.message(span.line, span.col, "depends on", "")
.as_str();
}
msg += "cyclic evaluation";
let refr = Self::get_rule_refr(rule);
let span = refr.span();
return Err(span.source.error(
span.line,
span.col,
format!("recursion detected when evaluating rule:{msg}").as_str(),
));
}
// Back up local variables of current function and empty
// the local variables of callee function.
let scopes = std::mem::take(&mut self.scopes);
let prev_module = self.set_current_module(Some(module.clone()))?;
fn eval_rule_impl(&mut self, module: &Ref<Module>, rule: &Ref<Rule>) -> Result<()> {
match rule.as_ref() {
Rule::Spec {
span,
@@ -3130,10 +3121,53 @@ impl Interpreter {
}
_ => bail!("internal error: unexpected"),
}
Ok(())
}
pub fn eval_rule(&mut self, module: &Ref<Module>, rule: &Ref<Rule>) -> Result<()> {
// Skip reprocessing rule
if self.processed.contains(rule) {
return Ok(());
}
// Skip default rules
if let Rule::Default { .. } = rule.as_ref() {
return Ok(());
}
self.active_rules.push(rule.clone());
if self.active_rules.iter().filter(|&r| r == rule).count() == 2 {
let mut msg = String::default();
for r in &self.active_rules {
let refr = Self::get_rule_refr(r);
let span = refr.span();
msg += span
.source
.message(span.line, span.col, "depends on", "")
.as_str();
}
msg += "cyclic evaluation";
self.active_rules.pop();
let refr = Self::get_rule_refr(rule);
let span = refr.span();
return Err(span.source.error(
span.line,
span.col,
format!("recursion detected when evaluating rule:{msg}").as_str(),
));
}
// Back up local variables of current function and empty
// the local variables of callee function.
let scopes = std::mem::take(&mut self.scopes);
let prev_module = self.set_current_module(Some(module.clone()))?;
let res = self.eval_rule_impl(module, rule);
self.set_current_module(prev_module)?;
self.scopes = scopes;
match self.active_rules.pop() {
Some(ref r) if r == rule => Ok(()),
Some(ref r) if r == rule => res,
_ => bail!("internal error: current rule not active"),
}
}
@@ -3406,4 +3440,18 @@ impl Interpreter {
}
Ok(())
}
pub fn add_extension(
&mut self,
path: String,
nargs: u8,
extension: Box<dyn Extension>,
) -> Result<()> {
if let std::collections::hash_map::Entry::Vacant(v) = self.extensions.entry(path) {
v.insert((nargs, extension));
Ok(())
} else {
bail!("extension already added");
}
}
}

View File

@@ -260,6 +260,37 @@ pub struct QueryResults {
pub result: Vec<QueryResult>,
}
/// A user defined builtin function implementation.
///
/// It is not necessary to implement this trait directly.
pub trait Extension: FnMut(Vec<Value>) -> anyhow::Result<Value> {
/// 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>
where
Self: 'a;
}
/// Automatically make matching closures a valid [`Extension`].
impl<F> Extension for F
where
F: FnMut(Vec<Value>) -> anyhow::Result<Value> + Clone,
{
fn clone_box<'a>(&self) -> Box<dyn 'a + Extension>
where
Self: 'a,
{
Box::new(self.clone())
}
}
/// Implement clone for a boxed extension using [`Extension::clone_box`].
impl<'a> Clone for Box<dyn 'a + Extension> {
fn clone(&self) -> Self {
(**self).clone_box()
}
}
/// Items in `unstable` are likely to change.
#[doc(hidden)]
pub mod unstable {

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()),
);
}
}
}

103
tests/engine/mod.rs Normal file
View File

@@ -0,0 +1,103 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use anyhow::{bail, Result};
use regorus::*;
#[test]
fn extension() -> Result<()> {
fn repeat(mut params: Vec<Value>) -> Result<Value> {
match params.remove(0) {
Value::String(s) => {
let s = s.as_ref().to_owned();
Ok(Value::from(s.clone() + &s))
}
_ => bail!("param must be string"),
}
}
let mut engine = Engine::new();
engine.add_policy(
"test.rego".to_string(),
r#"package test
x = repeat("hello")
"#
.to_string(),
)?;
// Raises error since repeat is not defined.
assert!(engine.eval_query("data.test.x".to_string(), false).is_err());
// Register extension.
engine.add_extension("repeat".to_string(), 1, Box::new(repeat))?;
// Adding extension twice is error.
assert!(engine
.add_extension(
"repeat".to_string(),
1,
Box::new(|_| { Ok(Value::Undefined) })
)
.is_err());
let r = engine.eval_query("data.test.x".to_string(), false)?;
assert_eq!(
r.result[0].expressions[0].value.as_string()?.as_ref(),
"hellohello"
);
Ok(())
}
#[test]
fn extension_with_state() -> Result<()> {
#[derive(Clone)]
struct Gen {
n: i64,
}
let mut engine = Engine::new();
engine.add_policy(
"test.rego".to_string(),
r#"package test
x = gen()
"#
.to_string(),
)?;
let mut g = Box::new(Gen { n: 5 });
engine.add_extension(
"gen".to_string(),
0,
Box::new(move |_: Vec<Value>| {
let v = Value::from(g.n);
g.n += 1;
Ok(v)
}),
)?;
// First eval.
let r = engine.eval_query("data.test.x".to_string(), false)?;
assert_eq!(r.result[0].expressions[0].value.as_i64()?, 5);
// Second eval will produce a new value since for each query, the
// internal evaluation state of the interpreter is cleared.
// This might change in the future.
let r = engine.eval_query("data.test.x".to_string(), false)?;
assert_eq!(r.result[0].expressions[0].value.as_i64()?, 6);
// Clone the engine.
// This should also clone the stateful extension.
let mut engine1 = engine.clone();
// Both the engines should produce the same value.
let r = engine.eval_query("data.test.x".to_string(), false)?;
let r1 = engine1.eval_query("data.test.x".to_string(), false)?;
assert_eq!(
r.result[0].expressions[0].value,
r1.result[0].expressions[0].value
);
assert_eq!(r.result[0].expressions[0].value.as_i64()?, 7);
Ok(())
}

View File

@@ -53,6 +53,23 @@ cases:
- 58
- 45
- note: leap-year
data: {}
modules:
- |
package test
a := time.diff(time.parse_ns("2006-01-02", "2020-02-02"), time.parse_ns("2006-01-02", "2020-03-01"))
query: data.test
want_result:
a:
- 0
- 0
- 28
- 0
- 0
- 0
- note: invalid-type
data: {}
modules:

View File

@@ -34,8 +34,8 @@ cases:
- |
package test
a := time.format([1703444325734390000, "UTC", "%Y-%m-%dT%H:%M:%S"])
b := time.format([1257894000000000000, "", "%d/%m/%Y %H:%M"])
a := time.format([1703444325734390000, "UTC", "2006-01-02T15:04:05"])
b := time.format([1257894000000000000, "", "02/01/2006 15:04"])
query: data.test
want_result:
a: "2023-12-24T18:58:45"

View File

@@ -8,11 +8,11 @@ cases:
- |
package test
a := time.parse_ns("%Y-%m-%dT%H:%M:%S", "2006-01-02T15:04:05")
b := time.parse_ns("%Y-%m-%d %H:%M:%S", "2015-09-05 23:56:04")
a := time.parse_ns("2006-01-02T15:04:05", "2016-05-10T19:06:42")
b := time.parse_ns("2006-01-02 15:04:05", "2015-09-05 23:56:04")
query: data.test
want_result:
a: 1136214245000000000
a: 1462907202000000000
b: 1441497364000000000
- note: format-and-parse-back
@@ -22,8 +22,8 @@ cases:
package test
a := res {
date := time.format([1703444325734390000, "UTC", "%Y-%m-%dT%H:%M:%S%.f"])
res := time.parse_ns("%Y-%m-%dT%H:%M:%S%.f", date)
date := time.format([1703444325734390000, "UTC", "2006-01-02T15:04:05.999999999"])
res := time.parse_ns("2006-01-02T15:04:05.999999999", date)
}
query: data.test
want_result:
@@ -35,6 +35,6 @@ cases:
- |
package test
a := time.parse_ns("%Y-%m-%dT%H:%M:%S%.f", 1703444325734390000)
a := time.parse_ns("2006-01-02T15:04:05.999999999", 1703444325734390000)
query: data.test
error: '`time.parse_ns` expects string argument. Got `1703444325734390000` instead'

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

@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
mod engine;
mod lexer;
mod parser;
mod value;

View File

@@ -95,6 +95,7 @@ subset
toarray
topdowndynamicdispatch
toset
time
trim
trimleft
trimprefix

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: |