mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
Compare commits
10 Commits
regorus-v0
...
regorus-v0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb1b25ff2f | ||
|
|
5fd826d79a | ||
|
|
d4dcbe7b9e | ||
|
|
fc68bf9c8b | ||
|
|
a95a9d21b3 | ||
|
|
5717f9c249 | ||
|
|
dda525b989 | ||
|
|
22260ac46f | ||
|
|
25b1ffe6d7 | ||
|
|
bcde71b8f0 |
35
.devcontainer/devcontainer.json
Normal file
35
.devcontainer/devcontainer.json
Normal 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
11
.github/dependabot.yml
vendored
Normal 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"
|
||||
14
.github/workflows/rust.yml
vendored
14
.github/workflows/rust.yml
vendored
@@ -36,12 +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: Install wasm-pack
|
||||
# run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
|
||||
- name: Run wasm binding tests
|
||||
run: |
|
||||
cd bindings/wasm
|
||||
wasm-pack test --node -r
|
||||
# - name: Run wasm binding tests
|
||||
# run: |
|
||||
# cd bindings/wasm
|
||||
# wasm-pack test --node -r
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[workspace]
|
||||
|
||||
members = [
|
||||
"bindings/ffi",
|
||||
"bindings/python",
|
||||
"bindings/wasm"
|
||||
]
|
||||
@@ -8,7 +9,7 @@ members = [
|
||||
[package]
|
||||
name = "regorus"
|
||||
description = "A fast, lightweight Rego (OPA policy language) interpreter"
|
||||
version = "0.1.0-alpha.3"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license-file = "LICENSE"
|
||||
repository = "https://github.com/microsoft/regorus"
|
||||
@@ -65,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"
|
||||
|
||||
38
README.md
38
README.md
@@ -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,7 +69,6 @@ $ 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.61.0 test-suite](https://www.openpolicyagent.org/docs/latest/ir/#test-suite) barring a few
|
||||
builtins. See [OPA Conformance](#opa-conformance) below.
|
||||
|
||||
@@ -65,8 +76,22 @@ builtins. See [OPA Conformance](#opa-conformance) below.
|
||||
|
||||
Regorus can be used from a variety of languages:
|
||||
|
||||
- Javascript: To compile Regorus to WASM and use it in Javascript, see [bindings/wasm](bindings/wasm)
|
||||
- Python: To use Regorus from Python, see [bindings/python](bindings/python)
|
||||
- *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
|
||||
|
||||
@@ -244,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
27
bindings/c/CMakeLists.txt
Normal 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
55
bindings/c/main.c
Normal 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;
|
||||
}
|
||||
28
bindings/cpp/CMakeLists.txt
Normal file
28
bindings/cpp/CMakeLists.txt
Normal 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
118
bindings/cpp/main.cpp
Normal 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
101
bindings/cpp/regorus.hpp
Normal 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
|
||||
51
bindings/csharp/Program.cs
Normal file
51
bindings/csharp/Program.cs
Normal 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
169
bindings/csharp/Regorus.cs
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
24
bindings/csharp/regorus-test.csproj
Normal file
24
bindings/csharp/regorus-test.csproj
Normal 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
13
bindings/ffi/CHANGELOG.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Changelog
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.1.0](https://github.com/microsoft/regorus/releases/tag/regorus-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
17
bindings/ffi/Cargo.toml
Normal 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"
|
||||
88
bindings/ffi/RegorusFFI.g.cs
Normal file
88
bindings/ffi/RegorusFFI.g.cs
Normal 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
32
bindings/ffi/build.rs
Normal 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
158
bindings/ffi/cbindgen.toml
Normal 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 = []
|
||||
95
bindings/ffi/regorus.ffi.hpp
Normal file
95
bindings/ffi/regorus.ffi.hpp
Normal 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
127
bindings/ffi/regorus.h
Normal 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
248
bindings/ffi/src/lib.rs
Normal 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
3
bindings/go/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module regorus-test
|
||||
|
||||
go 1.21.5
|
||||
58
bindings/go/main.go
Normal file
58
bindings/go/main.go
Normal 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)
|
||||
}
|
||||
117
bindings/go/pkg/regorus/mod.go
Normal file
117
bindings/go/pkg/regorus/mod.go
Normal 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
|
||||
}
|
||||
@@ -64,6 +64,8 @@ impl Engine {
|
||||
}
|
||||
|
||||
/// 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(())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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, ¶ms[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, ¶ms[0], &args[0])?;
|
||||
let value = ensure_string(name, ¶ms[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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ 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;
|
||||
@@ -348,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 ¶ms[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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2101,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)) => {
|
||||
@@ -2121,6 +2124,7 @@ impl Interpreter {
|
||||
_ => orig_fcn_path.clone(),
|
||||
};
|
||||
|
||||
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())),
|
||||
@@ -2134,6 +2138,11 @@ impl Interpreter {
|
||||
// process default functions later.
|
||||
(&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 Some(builtin) = self.lookup_builtin(span, &fcn_path)? {
|
||||
let r = self.eval_builtin_call(span, &fcn_path.clone(), *builtin, params);
|
||||
@@ -2153,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();
|
||||
@@ -3016,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,
|
||||
@@ -3133,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"),
|
||||
}
|
||||
}
|
||||
@@ -3409,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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
31
src/lib.rs
31
src/lib.rs
@@ -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 {
|
||||
|
||||
103
tests/engine/mod.rs
Normal file
103
tests/engine/mod.rs
Normal 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(())
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
mod engine;
|
||||
mod lexer;
|
||||
mod parser;
|
||||
mod value;
|
||||
|
||||
@@ -95,6 +95,7 @@ subset
|
||||
toarray
|
||||
topdowndynamicdispatch
|
||||
toset
|
||||
time
|
||||
trim
|
||||
trimleft
|
||||
trimprefix
|
||||
|
||||
Reference in New Issue
Block a user