diff --git a/.github/skills/bump-version/SKILL.md b/.github/skills/bump-version/SKILL.md index 784e18e4..5c097220 100644 --- a/.github/skills/bump-version/SKILL.md +++ b/.github/skills/bump-version/SKILL.md @@ -26,6 +26,9 @@ library and its wrapper packages. | `wrappers/python/setup.py` | `VERSION = "X.Y.Z"` | `VERSION = "1.3.0"` | | `wrappers/rust/regorust/Cargo.toml` | `version = "X.Y.Z"` | `version = "1.3.0"` | | `wrappers/dotnet/Rego/Rego.csproj` | `X.Y.Z` | `1.3.0` | +| `examples/rust/Cargo.toml` | `regorust = { version = "X.Y.Z" }` | `regorust = { version = "1.3.0" }` | +| `examples/dotnet/example/example.csproj` | `Version="X.Y.Z"` | `Version="1.3.0"` | +| `examples/dotnet/MyPolicy/MyPolicy.csproj` | `Version="X.Y.Z"` | `Version="1.3.0"` | ## Procedure diff --git a/.github/workflows/pr_gate.yml b/.github/workflows/pr_gate.yml index f2c9ad9d..6f644c15 100644 --- a/.github/workflows/pr_gate.yml +++ b/.github/workflows/pr_gate.yml @@ -197,6 +197,105 @@ jobs: working-directory: ${{github.workspace}}/wrappers/python/ run: pytest -vv + linux-example-cpp: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Get dependencies + run: | + sudo apt-get update + sudo apt-get install -y ninja-build libssl-dev + + - name: CMake config + working-directory: ${{github.workspace}}/examples/cpp + run: cmake -B ${{github.workspace}}/examples/cpp/build --preset release -G Ninja + env: + REGOCPP_REPO: LOCAL + + - name: CMake build + working-directory: ${{github.workspace}}/examples/cpp/build + run: ninja install + + - name: CMake test + working-directory: ${{github.workspace}}/examples/cpp/build + run: ctest -V --build-config Release --timeout 120 --output-on-failure -T Test + + linux-example-python: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Use Python + uses: actions/setup-python@v6 + with: + python-version: "3.12" + + - name: Get dependencies + run: | + sudo apt-get update + sudo apt-get install -y libssl-dev + python -m pip install --upgrade pip + + - name: Python build + working-directory: ${{github.workspace}}/wrappers/python/ + run: pip install -e .[dev] + env: + REGOCPP_REPO: LOCAL + + - name: Run example + working-directory: ${{github.workspace}}/examples/python + run: python example.py + + linux-example-rust: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Get dependencies + run: | + sudo apt-get update + sudo apt-get install -y cargo rustc libssl-dev + + - name: Cargo run + working-directory: ${{github.workspace}}/examples/rust + run: cargo run --config 'patch.crates-io.regorust.path="../../wrappers/rust/regorust"' + env: + REGOCPP_REPO: LOCAL + + linux-example-dotnet: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Get dependencies + run: | + sudo apt-get update + sudo apt-get install -y libssl-dev + + - name: Use dotnet + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 8.0.x + + - name: Patch project reference + working-directory: ${{github.workspace}}/examples/dotnet/example + run: sed -i 's|||' example.csproj + + - name: Run example + working-directory: ${{github.workspace}}/examples/dotnet/example + run: dotnet run + env: + REGOCPP_REPO: LOCAL + linux-cheriot: runs-on: ubuntu-latest diff --git a/README.md b/README.md index 7e45c450..b20ce084 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,35 @@ You can run the test driver from the same directory: ./bin/rego_test tests/regocpp.yaml +### Installing with vcpkg + +You can install rego-cpp using [vcpkg](https://vcpkg.io/): + +```bash +vcpkg install rego-cpp +``` + +To enable crypto/JWT built-ins (requires OpenSSL 3): + +```bash +vcpkg install rego-cpp[openssl3] +``` + +To also install the `rego` CLI tool: + +```bash +vcpkg install rego-cpp[openssl3,tools] +``` + +Then in your CMake project: + +```cmake +find_package(regocpp CONFIG REQUIRED) +target_link_libraries( PRIVATE regocpp::rego) +``` + +For overlay port usage, see [`ports/rego-cpp/`](ports/rego-cpp/). + ### Using the `rego` Library See the [examples](examples/README.md) directory for examples of how to use the diff --git a/examples/README.md b/examples/README.md index 3004c12e..7f8d414a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -50,51 +50,48 @@ a@host:dist$ ./bin/regoc -d examples/scalars.rego -q data.scalars.greeting ``` ## Python -The Python example is a [simple command line tool](python/rego.py) that takes zero or more -Rego data, module, and input files and evaluates a query against them. - -Examples: +The [Python example](python/example.py) is a demonstration program that shows +how to use all the features of the wrapper, including queries, input/data, +and bundles. ```bash a@host:python$ pip install regopy -a@host:python$ python rego.py eval -d examples/scalars.rego data.scalars.greeting -{"expressions":["Hello"]} - -a@host:python$ python rego.py eval -d examples/objects.rego data.objects.sites[1].name -{"expressions":["smoke1"]} - -a@host:python$ python rego.py eval -d examples/data0.json -d examples/data1.json -d examples/objects.rego -i examples/input0.json "[data.one, input.b, data.objects.sites[1]]" -{"expressions":[[{"bar":"Foo", "baz":5, "be":true, "bop":23.4},"20",{"name":"smoke1"}]]} - -a@host:python$ python rego.py eval "x=5; y=x + (2 - 4 * 0.25) * -3 + 7.4;2 * 5" +a@host:python$ python example.py +Query Only {"expressions":[true, true, 10], "bindings":{"x":5, "y":9.4}} +x = 5 +10 -a@host:python$ python rego.py eval -d examples/bodies.rego -i examples/input1.json data.bodies.e -{"expressions":[{"one":15, "two":15}]} -``` +Input and Data +{"expressions":[true], "bindings":{"x":[{"bar":"Foo", "baz":5, "be":true, "bop":23.4},"20",{"name":"smoke1"}]}} +Bundles +query: {"expressions":[true], "bindings":{"x":4460}} +example/foo: {"expressions":[2275]} +``` -## Rust -The Rust example is another [simple command line tool](rust/src/main.rs) that takes zero or more +There is also a [command line tool](python/rego.py) that takes zero or more Rego data, module, and input files and evaluates a query against them. -Examples: - -```bash -a@host:rust$ cargo run -- eval -d examples/scalars.rego data.scalars.greeting -{"expressions":["Hello"]} - -a@host:rust$ cargo run -- eval -d examples/objects.rego data.objects.sites[1].name -{"expressions":["smoke1"]} -a@host:rust$ cargo run -- eval -d examples/data0.json -d examples/data1.json -d examples/objects.rego -i examples/input0.json "[data.one, input.b, data.objects.sites[1]]" -{"expressions":[[{"bar":"Foo", "baz":5, "be":true, "bop":23.4},"20",{"name":"smoke1"}]]} +## Rust +The [Rust example](rust/src/main.rs) is a demonstration program that shows +how to use all the features of the wrapper, including queries, input/data, +and bundles. -a@host:rust$ cargo run -- eval "x=5; y=x + (2 - 4 * 0.25) * -3 + 7.4;5 * 2" +```bash +a@host:rust$ cargo run +Query Only {"expressions":[true, true, 10], "bindings":{"x":5, "y":9.4}} +x = 5 +10 + +Input and Data +{"expressions":[true], "bindings":{"x":[{"bar":"Foo", "baz":5, "be":true, "bop":23.4},"20",{"name":"smoke1"}]}} -a@host:rust$ cargo run -- eval -d examples/bodies.rego -i examples/input1.json data.bodies.e -{"expressions":[{"one":15, "two":15}]} +Bundles +query: {"expressions":[true], "bindings":{"x":4460}} +example/foo: {"expressions":[2275]} ``` ## dotnet diff --git a/examples/c/CMakeLists.txt b/examples/c/CMakeLists.txt index be7ca965..4f7d84ca 100644 --- a/examples/c/CMakeLists.txt +++ b/examples/c/CMakeLists.txt @@ -57,4 +57,9 @@ add_test( COMMAND bin/regoc -d examples/data0.json -d examples/data1.json -i examples/input0.json -d examples/objects.rego -q data.objects.sites[1] WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/dist ) -set_tests_properties(regoc PROPERTIES PASS_REGULAR_EXPRESSION "{\"name\":\"smoke1\"}") \ No newline at end of file +set_tests_properties(regoc PROPERTIES PASS_REGULAR_EXPRESSION "{\"name\":\"smoke1\"}") +add_test( + NAME example + COMMAND bin/example + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/dist +) \ No newline at end of file diff --git a/examples/cpp/CMakeLists.txt b/examples/cpp/CMakeLists.txt index ed77e2af..9b8713d7 100644 --- a/examples/cpp/CMakeLists.txt +++ b/examples/cpp/CMakeLists.txt @@ -51,4 +51,9 @@ add_test( COMMAND bin/custom_builtin WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/dist ) -set_tests_properties(custom_builtin PROPERTIES PASS_REGULAR_EXPRESSION "5") \ No newline at end of file +set_tests_properties(custom_builtin PROPERTIES PASS_REGULAR_EXPRESSION "5") +add_test( + NAME example + COMMAND bin/example + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/dist +) \ No newline at end of file diff --git a/examples/cpp/example.cc b/examples/cpp/example.cc index b783bcad..546609df 100644 --- a/examples/cpp/example.cc +++ b/examples/cpp/example.cc @@ -1,5 +1,3 @@ -#include "trieste/logging.h" - #include int main() diff --git a/examples/python/example.py b/examples/python/example.py index 0d533004..0f76fda2 100644 --- a/examples/python/example.py +++ b/examples/python/example.py @@ -2,10 +2,34 @@ from regopy import Input, Interpreter +############################## +##### Query Only ##### +############################## + +print("Query Only") + +# You can run simple queries without any input or data. rego = Interpreter() -with open("examples/objects.rego") as f: - rego.add_module("objects.rego", f.read()) +output = rego.query("x=5;y=x + (2 - 4 * 0.25) * -3 + 7.4;2 * 5") +print(output) +# {"expressions":[true, true, 10], "bindings":{"x":5, "y":9.4}} + +# You can access bound results using the binding method +print("x =", output.binding("x").json()) + +# You can also access expressions by index +print(output[0][2]) + +print() + +############################## +#### Input and Data ##### +############################## +print("Input and Data") + +# If you provide a dict, it will be converted to JSON before +# being added to the state. rego.add_data({ "one": { "bar": "Foo", @@ -21,26 +45,102 @@ } }) -rego.add_data({ +# You can also provide JSON directly. +rego.add_data_json(""" +{ "three": { "bar": "Baz", "baz": 15, - "be": True, + "be": true, "bop": 4.23 } -}) +} +""") + +objects_source = """ +package objects -rego.set_input(Input({ +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"] +""" +rego.add_module("objects.rego", objects_source) + +# Inputs can be either JSON or Rego, and provided +# as objects or as text. +rego.set_input_term(""" +{ "a": 10, "b": "20", "c": 30.0, - "d": True -})) + "d": true +} +""") print(rego.query("[data.one, input.b, data.objects.sites[1]] = x")) +# {"bindings":{"x":[{"bar":"Foo", "baz":5, "be":true, "bop":23.4}, "20", {"name":"smoke1"}]}} + +############################## +##### Bundles ##### +############################## + +print() +print("Bundles") + +# If you want to run the same set of queries against a policy with different +# inputs, you can create a bundle and use that to save the cost of compilation. + +rego_build = Interpreter() + +rego_build.add_data_json(""" +{"a": 7, +"b": 13} +""") + +rego_build.add_module("example.rego", """ +package example + +foo := data.a * input.x + data.b * input.y +bar := data.b * input.x + data.a * input.y +""") + +# We can specify both a default query, and specific entry points into the policy +# that should be made available to use later. +bundle = rego_build.build( + "x=data.example.foo + data.example.bar", + ["example/foo", "example/bar"] +) + +# We can now save the bundle to disk +rego_build.save_bundle("bundle", bundle) + +# And load it again +rego_run = Interpreter() +rego_run.load_bundle("bundle") -bundle = rego.build("[data.one, input.b, data.objects.sites[1]] = x", ["objects/sites"]) +# The most efficient way to provide input to a policy is by constructing it +# manually, without the need for parsing JSON or Rego. +rego_run.set_input(Input({"x": 104, "y": 119})) -print(rego.query_bundle(bundle)) +# We can query the bundle, which will use the entrypoint of the default query +# provided at build +print("query:", rego_run.query_bundle(bundle)) +# query: {"expressions":[true], "bindings":{"x":4460}} -print(rego.query_bundle_entrypoint(bundle, "objects/sites")) \ No newline at end of file +# Or we can query specific entrypoints +print("example/foo:", rego_run.query_bundle_entrypoint(bundle, "example/foo")) +# example/foo: {"expressions":[2275]} diff --git a/examples/rust/Cargo.toml b/examples/rust/Cargo.toml index 9be031ef..b3cfadd2 100644 --- a/examples/rust/Cargo.toml +++ b/examples/rust/Cargo.toml @@ -6,5 +6,4 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -regorust = { version = "1.2.0" } -clap = { version = "4.0", features = ["derive"] } \ No newline at end of file +regorust = { version = "1.4.0" } \ No newline at end of file diff --git a/examples/rust/src/main.rs b/examples/rust/src/main.rs index a6c22e65..a88b5b11 100644 --- a/examples/rust/src/main.rs +++ b/examples/rust/src/main.rs @@ -1,197 +1,193 @@ -use clap::{Parser, Subcommand, ValueEnum}; -use regorust::{BundleFormat, Interpreter, LogLevel}; - -#[derive(Parser)] -#[command(author, version, about, long_about = None)] -struct Args { - #[arg(short, long)] - loglevel: Option, - - #[command(subcommand)] - command: Commands, -} - -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] -enum LogLevelOption { - None, - Debug, - Info, - Warn, - Error, - Output, - Trace, -} - -#[derive(Subcommand)] -enum Commands { - Eval { - query: String, - - #[arg(short, long)] - data: Vec, - - #[arg(short, long)] - input: Option, - }, - - Build { - #[arg(short, long)] - query: Option, - - #[arg(short, long)] - data: Vec, - - #[arg(short, long)] - entrypoints: Vec, - - #[arg(short, long)] - bundle: Option, - - #[arg(long)] - binary: bool, - }, - - Run { - #[arg(short, long)] - input: Option, - - #[arg(short, long)] - entrypoint: Option, - - #[arg(short, long)] - bundle: Option, - - #[arg(long)] - binary: bool, - }, -} +use regorust::{BundleFormat, Input, Interpreter}; +use std::path::Path; fn main() { - let args = Args::parse(); - let default_path = std::path::PathBuf::from("bundle"); - let rego = Interpreter::new(); + ////////////////////// + ///// Query Only ///// + ////////////////////// - match args.loglevel { - Some(LogLevelOption::None) => rego.set_log_level(LogLevel::None), - Some(LogLevelOption::Debug) => rego.set_log_level(LogLevel::Debug), - Some(LogLevelOption::Info) => rego.set_log_level(LogLevel::Info), - Some(LogLevelOption::Warn) => rego.set_log_level(LogLevel::Warn), - Some(LogLevelOption::Error) => rego.set_log_level(LogLevel::Error), - Some(LogLevelOption::Output) => rego.set_log_level(LogLevel::Output), - Some(LogLevelOption::Trace) => rego.set_log_level(LogLevel::Trace), - None => rego.set_log_level(LogLevel::Output), - } - .expect("Unable to set log level"); - - match &args.command { - Commands::Eval { query, data, input } => { - if let Some(i) = input { - rego.set_input_json_file(i.as_path()) - .expect("Failed to read input file"); - } - - for d in data { - if d.extension().unwrap() == "rego" { - rego.add_module_file(d.as_path()) - .expect("Failed to load module file"); - } else { - rego.add_data_json_file(d.as_path()) - .expect("Failed to load data file"); - } - } - - println!( - "{}", - rego.query(query.as_str()) - .expect("Failed to evaluate query") - ); - } + println!("Query Only"); - Commands::Build { - query, - data, - entrypoints, - bundle, - binary, - } => { - for d in data { - if d.extension().unwrap() == "rego" { - rego.add_module_file(d.as_path()) - .expect("Failed to load module file"); - } else { - rego.add_data_json_file(d.as_path()) - .expect("Failed to load data file"); - } - } - - let bundle_path = if let Some(b) = bundle { - b.as_path() - } else { - default_path.as_path() - }; - - match rego.build(query, entrypoints) { - Ok(b) => { - rego.save_bundle( - bundle_path, - &b, - if *binary { - BundleFormat::Binary - } else { - BundleFormat::JSON - }, - ) - .expect("Unable to save bundle"); - println!("Bundle built successfully"); - } - - Err(msg) => { - println!("Unable to build bundle: {}", msg); - } - } + // You can run simple queries without any input or data. + let rego = Interpreter::new(); + let output = rego + .query("x=5;y=x + (2 - 4 * 0.25) * -3 + 7.4;2 * 5") + .expect("Failed to evaluate query"); + println!("{}", output); + // {"expressions":[true, true, 10], "bindings":{"x":5, "y":9.4}} + + // You can access bound results using the binding method + let x = output.binding("x").expect("x is not bound"); + println!("x = {}", x.json().unwrap()); + + // You can also access expressions by index + let exprs = output.expressions().expect("no expressions"); + println!("{}", exprs.index(2).unwrap().json().unwrap()); + + println!(); + + //////////////////////// + //// Input and Data //// + //////////////////////// + + println!("Input and Data"); + + // You can provide JSON data directly. + rego.add_data_json( + r#"{ + "one": { + "bar": "Foo", + "baz": 5, + "be": true, + "bop": 23.4 + }, + "two": { + "bar": "Bar", + "baz": 12.3, + "be": false, + "bop": 42 } - - Commands::Run { - input, - entrypoint, - bundle, - binary, - } => { - if let Some(i) = input { - rego.set_input_json_file(i.as_path()) - .expect("Failed to read input file"); - } - - let bundle_path = if let Some(b) = bundle { - b.as_path() - } else { - default_path.as_path() - }; - - let bundle = rego - .load_bundle( - bundle_path, - if *binary { - BundleFormat::Binary - } else { - BundleFormat::JSON - }, - ) - .expect("Unable to load bundle"); - - if let Some(e) = entrypoint { - println!( - "{}", - rego.query_bundle_entrypoint(&bundle, e.as_str()) - .expect("Failed to query bundle") - ); - return; - } else { - println!( - "{}", - rego.query_bundle(&bundle).expect("Failed to query bundle") - ); - } + }"#, + ) + .expect("Failed to add data"); + + rego.add_data_json( + r#"{ + "three": { + "bar": "Baz", + "baz": 15, + "be": true, + "bop": 4.23 } - } + }"#, + ) + .expect("Failed to add data"); + + rego.add_module( + "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"] +"#, + ) + .expect("Failed to add module"); + + // Inputs can be either JSON or Rego, and provided as text. + rego.set_input_json( + r#"{ + "a": 10, + "b": "20", + "c": 30.0, + "d": true + }"#, + ) + .expect("Failed to set input"); + + println!( + "{}", + rego.query("[data.one, input.b, data.objects.sites[1]] = x") + .expect("Failed to evaluate query") + ); + // {"bindings":{"x":[{"bar":"Foo", "baz":5, "be":true, "bop":23.4}, "20", {"name":"smoke1"}]}} + + /////////////////// + ///// Bundles //// + /////////////////// + + println!(); + println!("Bundles"); + + // If you want to run the same set of queries against a policy with different + // inputs, you can create a bundle and use that to save the cost of compilation. + + let rego_build = Interpreter::new(); + + rego_build + .add_data_json( + r#"{"a": 7, +"b": 13}"#, + ) + .expect("Failed to add data"); + + rego_build + .add_module( + "example.rego", + r#"package example + +foo := data.a * input.x + data.b * input.y +bar := data.b * input.x + data.a * input.y +"#, + ) + .expect("Failed to add module"); + + // We can specify both a default query, and specific entry points into the policy + // that should be made available to use later. + let bundle = rego_build + .build( + &Some("x=data.example.foo + data.example.bar"), + &["example/foo", "example/bar"], + ) + .expect("Failed to build bundle"); + + // We can now save the bundle to disk + let bundle_path = Path::new("bundle"); + rego_build + .save_bundle(bundle_path, &bundle, BundleFormat::JSON) + .expect("Failed to save bundle"); + + // And load it again + let rego_run = Interpreter::new(); + rego_run + .load_bundle(bundle_path, BundleFormat::JSON) + .expect("Failed to load bundle"); + + // The most efficient way to provide input to a policy is by constructing it + // manually, without the need for parsing JSON or Rego. + let input = Input::new() + .str("x") + .int(104) + .objectitem() + .str("y") + .int(119) + .objectitem() + .object(2) + .validate() + .expect("Failed to create input"); + + rego_run.set_input(&input).expect("Failed to set input"); + + // We can query the bundle, which will use the entrypoint of the default query + // provided at build + println!( + "query: {}", + rego_run + .query_bundle(&bundle) + .expect("Failed to query bundle") + ); + // query: {"expressions":[true], "bindings":{"x":4460}} + + // Or we can query specific entrypoints + println!( + "example/foo: {}", + rego_run + .query_bundle_entrypoint(&bundle, "example/foo") + .expect("Failed to query bundle entrypoint") + ); + // example/foo: {"expressions":[2275]} }