diff --git a/Cargo.toml b/Cargo.toml index 50237ca2..5021f1f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "bindings/python", "bindings/wasm", "bindings/java", + "bindings/ruby/ext/regorusrb", ] [package] diff --git a/README.md b/README.md index b2210a84..2d901c9b 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Regorus is also - *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*, *Java*, *Javascript* and *Python*. + - *polyglot* - In addition to Rust, Regorus can be used from *C*, *C++*, *C#*, *Golang*, *Java*, *Javascript*, *Python*, and *Ruby*. 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/). @@ -90,6 +90,8 @@ Regorus can be used from a variety of languages: - *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/). +- *Ruby*: Ruby bindings are developed using [magnus](https://github.com/matsadler/magnus). + See [bindings/ruby](https://github.com/microsoft/regorus/tree/main/bindings/ruby). To avoid operational overhead, we currently don't publish these bindings to various repositories. It is straight-forward to build these bindings yourself. diff --git a/bindings/ruby/.gitignore b/bindings/ruby/.gitignore new file mode 100644 index 00000000..7c512ea9 --- /dev/null +++ b/bindings/ruby/.gitignore @@ -0,0 +1,14 @@ +/.bundle/ +/.yardoc +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ +*.bundle +*.so +*.o +*.a +mkmf.log +target/ diff --git a/bindings/ruby/.rubocop.yml b/bindings/ruby/.rubocop.yml new file mode 100644 index 00000000..8b9e9faf --- /dev/null +++ b/bindings/ruby/.rubocop.yml @@ -0,0 +1,29 @@ +require: + - rubocop-minitest + - rubocop-rake + +AllCops: + TargetRubyVersion: 3.0 + NewCops: enable + +Layout/LineLength: + Max: 180 + +Lint/EmptyClass: + Enabled: false + +Metrics/ClassLength: + Exclude: + - 'test/**/*.rb' + +Metrics/MethodLength: + Enabled: false + +Style/StringLiterals: + EnforcedStyle: double_quotes + +Style/StringLiteralsInInterpolation: + EnforcedStyle: double_quotes + +Style/WordArray: + Enabled: false diff --git a/bindings/ruby/.tool-versions b/bindings/ruby/.tool-versions new file mode 100644 index 00000000..3294aeda --- /dev/null +++ b/bindings/ruby/.tool-versions @@ -0,0 +1 @@ +ruby 3.3.0 diff --git a/bindings/ruby/CHANGELOG.md b/bindings/ruby/CHANGELOG.md new file mode 100644 index 00000000..66e3d2c0 --- /dev/null +++ b/bindings/ruby/CHANGELOG.md @@ -0,0 +1,5 @@ +## [Unreleased] + +## [0.1.0] - 2024-03-29 + +- Initial release diff --git a/bindings/ruby/Gemfile b/bindings/ruby/Gemfile new file mode 100644 index 00000000..21c6136f --- /dev/null +++ b/bindings/ruby/Gemfile @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +# Specify your gem's dependencies in regorusrb.gemspec +gemspec + +# These gems are required for local development and testing, +# but won't be included in the published gem +gem "minitest", "~> 5.16" +gem "rake", "~> 13.0" +gem "rake-compiler" +gem "rb_sys", "~> 0.9.63" +gem "rubocop", "~> 1.62", require: false +gem "rubocop-minitest", require: false +gem "rubocop-rake", require: false diff --git a/bindings/ruby/Gemfile.lock b/bindings/ruby/Gemfile.lock new file mode 100644 index 00000000..cc83c868 --- /dev/null +++ b/bindings/ruby/Gemfile.lock @@ -0,0 +1,61 @@ +PATH + remote: . + specs: + regorusrb (0.1.0) + +GEM + remote: https://rubygems.org/ + specs: + ast (2.4.2) + json (2.7.1) + language_server-protocol (3.17.0.3) + minitest (5.22.3) + parallel (1.24.0) + parser (3.3.0.5) + ast (~> 2.4.1) + racc + racc (1.7.3) + rainbow (3.1.1) + rake (13.1.0) + rake-compiler (1.2.7) + rake + rb_sys (0.9.90) + regexp_parser (2.9.0) + rexml (3.2.6) + rubocop (1.62.1) + json (~> 2.3) + language_server-protocol (>= 3.17.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.31.1, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.31.2) + parser (>= 3.3.0.4) + rubocop-minitest (0.35.0) + rubocop (>= 1.61, < 2.0) + rubocop-ast (>= 1.31.1, < 2.0) + rubocop-rake (0.6.0) + rubocop (~> 1.0) + ruby-progressbar (1.13.0) + unicode-display_width (2.5.0) + +PLATFORMS + ruby + x86_64-linux + +DEPENDENCIES + minitest (~> 5.16) + rake (~> 13.0) + rake-compiler + rb_sys (~> 0.9.63) + regorusrb! + rubocop (~> 1.62) + rubocop-minitest + rubocop-rake + +BUNDLED WITH + 2.5.7 diff --git a/bindings/ruby/LICENSE.txt b/bindings/ruby/LICENSE.txt new file mode 120000 index 00000000..30cff740 --- /dev/null +++ b/bindings/ruby/LICENSE.txt @@ -0,0 +1 @@ +../../LICENSE \ No newline at end of file diff --git a/bindings/ruby/README.md b/bindings/ruby/README.md new file mode 100644 index 00000000..6d2dc252 --- /dev/null +++ b/bindings/ruby/README.md @@ -0,0 +1,95 @@ +# Regorusrb + +**Regorus** is + + - *Rego*-*Rus(t)* - A fast, light-weight [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/) + interpreter written in Rust. + - *Rigorous* - A rigorous enforcer of well-defined Rego semantics. + +## Installation + +Regorus can be used in Ruby by configuring bundler to build from the remote git source. + +Use the bundler CLI to add the gem from remote git source: +` +bundle add regorus --git 'https://github.com/microsoft/regorus/tree/main/bindings/ruby' +` + +or manually edit your gemfile to include the following +` +gem "regorus", git: "https://github.com/microsoft/regorus/tree/main/bindings/ruby" +` + +It is not yet available in rubygems. + +See [Repository](https://github.com/microsoft/regorus). + +To build this gem locally without bundler, + +`rake build` + +then to install the gem and build the native extensions + +`gem install --local ./pkg/regorusrb-0.1.0.gem` + +## Usage + +```ruby +require "regorus" + +engine = Regorus::Engine.new + +engine.add_policy_from_file('../../tests/aci/framework.rego') +engine.add_policy_from_file('../../tests/aci/api.rego') +engine.add_policy_from_file('../../tests/aci/policy.rego') + + +# can be strings or symbols +data = { + metadata: { + devices: { + "/run/layers/p0-layer0": "1b80f120dbd88e4355d6241b519c3e25290215c469516b49dece9cf07175a766", + "/run/layers/p0-layer1": "e769d7487cc314d3ee748a4440805317c19262c7acd2fdbdb0d47d2e4613a15c", + "/run/layers/p0-layer2": "eb36921e1f82af46dfe248ef8f1b3afb6a5230a64181d960d10237a08cd73c79", + "/run/layers/p0-layer3": "41d64cdeb347bf236b4c13b7403b633ff11f1cf94dbc7cf881a44d6da88c5156", + "/run/layers/p0-layer4": "4dedae42847c704da891a28c25d32201a1ae440bce2aecccfa8e6f03b97a6a6c", + "/run/layers/p0-layer5": "fe84c9d5bfddd07a2624d00333cf13c1a9c941f3a261f13ead44fc6a93bc0e7a" + } + } +} + +engine.add_data(data) +input = { + "containerID": "container0", + "layerPaths": [ + "/run/layers/p0-layer0", + "/run/layers/p0-layer1", + "/run/layers/p0-layer2", + "/run/layers/p0-layer3", + "/run/layers/p0-layer4", + "/run/layers/p0-layer5" + ], + "target": "/run/gcs/c/container0/rootfs" +} + +engine.set_input(input) + +# Evaluate a specife rule +rule_results = engine.eval_rule('data.framework.mount_overlay') +puts rule_results # { "allowed" => true, "metadata" => [...]} + +# Or evalute a full policy document +query_results = engine.eval_query('data.framework') +puts query_results[:result][0] + +# Query results can can also be returned as JSON strings instead of Ruby Hash structure +results_json = engine.eval_query_as_json('data.framework.mount_overlay=x') +puts results_json +``` + +## Development + +After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. + +To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org). + diff --git a/bindings/ruby/Rakefile b/bindings/ruby/Rakefile new file mode 100644 index 00000000..9b8e4f25 --- /dev/null +++ b/bindings/ruby/Rakefile @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require "bundler/gem_tasks" +require "minitest/test_task" + +Minitest::TestTask.create + +require "rubocop/rake_task" + +RuboCop::RakeTask.new + +require "rb_sys/extensiontask" + +desc "build the .gem file, including native extensions, according to the .gemspec" +task build: :compile + +GEMSPEC = Gem::Specification.load("regorusrb.gemspec") + +RbSys::ExtensionTask.new("regorusrb", GEMSPEC) do |ext| + ext.lib_dir = "lib/regorusrb" +end + +task default: %i[compile test rubocop] diff --git a/bindings/ruby/bin/console b/bindings/ruby/bin/console new file mode 100755 index 00000000..84c91f45 --- /dev/null +++ b/bindings/ruby/bin/console @@ -0,0 +1,11 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "bundler/setup" +require "regorus" + +# You can add fixtures and/or initialization code here to make experimenting +# with your gem easier. You can also use a different console, if you like. + +require "irb" +IRB.start(__FILE__) diff --git a/bindings/ruby/bin/setup b/bindings/ruby/bin/setup new file mode 100755 index 00000000..dce67d86 --- /dev/null +++ b/bindings/ruby/bin/setup @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' +set -vx + +bundle install + +# Do any other automated setup that you need to do here diff --git a/bindings/ruby/ext/regorusrb/Cargo.toml b/bindings/ruby/ext/regorusrb/Cargo.toml new file mode 100644 index 00000000..8f5be434 --- /dev/null +++ b/bindings/ruby/ext/regorusrb/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "regorusrb" +version = "0.1.0" +edition = "2021" +description = "Ruby bindings for Regorus - a fast, lightweight Rego interpreter written in Rust" +publish = false + +[lib] +crate-type = ["cdylib"] +path = "src/lib.rs" + +[dependencies] +magnus = { version = "0.6.2" } +regorus = { path = "../../../.." } +serde_json = "1.0.115" +serde_magnus = "0.8.1" diff --git a/bindings/ruby/ext/regorusrb/extconf.rb b/bindings/ruby/ext/regorusrb/extconf.rb new file mode 100644 index 00000000..2e7d4a79 --- /dev/null +++ b/bindings/ruby/ext/regorusrb/extconf.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +require "mkmf" +require "rb_sys/mkmf" + +create_rust_makefile("regorusrb/regorusrb") diff --git a/bindings/ruby/ext/regorusrb/src/lib.rs b/bindings/ruby/ext/regorusrb/src/lib.rs new file mode 100644 index 00000000..ed8e419a --- /dev/null +++ b/bindings/ruby/ext/regorusrb/src/lib.rs @@ -0,0 +1,224 @@ +use magnus::{exception::runtime_error, method, module, prelude::*, Error, Ruby}; +use regorus::Engine as RegorusEngine; +use std::cell::RefCell; +use std::cmp::Ordering; + +// `Value` exists under magnus, regorus, and serde_json, so be explicit + +#[derive(Default)] +#[magnus::wrap(class = "Regorus::Engine")] +pub struct Engine { + engine: RefCell, +} + +impl Clone for Engine { + fn clone(&self) -> Self { + Self { + engine: self.engine.clone(), + } + } +} + +impl Engine { + fn initialize(&self) { + let engine = RegorusEngine::new(); + *self.engine.borrow_mut() = engine; + } + + fn compare(&self, other: &Self) -> Result { + let self_ptr: *const _ = &*self.engine.borrow(); + let other_ptr: *const _ = &*other.engine.borrow(); + match self_ptr.partial_cmp(&other_ptr) { + Some(Ordering::Less) => Ok(-1), + Some(Ordering::Equal) => Ok(0), + Some(Ordering::Greater) => Ok(1), + None => Err(Error::new(runtime_error(), "Comparison failed")), + } + } + + fn add_policy(&self, path: String, rego: String) -> Result<(), Error> { + self.engine + .borrow_mut() + .add_policy(path, rego) + .map_err(|e| Error::new(runtime_error(), format!("Failed to add policy: {}", e))) + } + + fn add_policy_from_file(&self, path: String) -> Result<(), Error> { + self.engine + .borrow_mut() + .add_policy_from_file(path) + .map_err(|e| Error::new(runtime_error(), format!("Failed to add policy: {}", e))) + } + + fn add_data(&self, ruby_hash: magnus::RHash) -> Result<(), Error> { + let data_value: regorus::Value = serde_magnus::deserialize(ruby_hash).map_err(|e| { + Error::new( + runtime_error(), + format!("Failed to deserialize Ruby value: {}", e), + ) + })?; + + self.engine + .borrow_mut() + .add_data(data_value) + .map_err(|e| Error::new(runtime_error(), format!("Failed to add data: {}", e))) + } + + fn add_data_json(&self, json_string: String) -> Result<(), Error> { + self.engine + .borrow_mut() + .add_data_json(&json_string) + .map_err(|e| Error::new(runtime_error(), format!("Failed to add data json: {}", e))) + } + + fn add_data_from_json_file(&self, path: String) -> Result<(), Error> { + let json_data = regorus::Value::from_json_file(&path).map_err(|e| { + Error::new( + runtime_error(), + format!("Failed to parse JSON data file: {}", e), + ) + })?; + + self.engine.borrow_mut().add_data(json_data).map_err(|e| { + Error::new( + runtime_error(), + format!("Failed to add data from file: {}", e), + ) + }) + } + + fn clear_data(&self) -> Result<(), Error> { + self.engine.borrow_mut().clear_data(); + Ok(()) + } + + fn set_input(&self, ruby_hash: magnus::RHash) -> Result<(), Error> { + let input_value: regorus::Value = serde_magnus::deserialize(ruby_hash).map_err(|e| { + Error::new( + runtime_error(), + format!("Failed to deserialize Ruby value: {}", e), + ) + })?; + + self.engine.borrow_mut().set_input(input_value); + Ok(()) + } + + fn set_input_json(&self, json_string: String) -> Result<(), Error> { + self.engine + .borrow_mut() + .set_input_json(&json_string) + .map_err(|e| Error::new(runtime_error(), format!("Failed to set input JSON: {}", e))) + } + + fn add_input_from_json_file(&self, path: String) -> Result<(), Error> { + let json_data = regorus::Value::from_json_file(&path).map_err(|e| { + Error::new( + runtime_error(), + format!("Failed to parse JSON input file: {}", e), + ) + })?; + + self.engine.borrow_mut().set_input(json_data); + Ok(()) + } + + fn eval_query(&self, query: String) -> Result { + let results = self + .engine + .borrow_mut() + .eval_query(query, false) + .map_err(|e| Error::new(runtime_error(), format!("Failed to evaluate query: {}", e)))?; + + serde_magnus::serialize(&results).map_err(|e| { + Error::new( + runtime_error(), + format!("Failed to serailzie query results: {}", e), + ) + }) + } + + fn eval_query_as_json(&self, query: String) -> Result { + let results = self + .engine + .borrow_mut() + .eval_query(query, false) + .map_err(|e| { + Error::new( + runtime_error(), + format!("Failed to evaluate query as json: {}", e), + ) + })?; + + serde_json::to_string(&results).map_err(|e| { + Error::new( + runtime_error(), + format!("Failed to serialize query results: {}", e), + ) + }) + } + + fn eval_rule(&self, path: String) -> Result, Error> { + let result = + self.engine.borrow_mut().eval_rule(path).map_err(|e| { + Error::new(runtime_error(), format!("Failed to evaluate rule: {}", e)) + })?; + + match result { + regorus::Value::Undefined => Ok(None), // Convert undefined to Ruby's nil + _ => serde_magnus::serialize(&result) // Serialize other results normally + .map(Some) + .map_err(|e| { + magnus::Error::new( + runtime_error(), + format!("Failed to serialize the rule evaluation result: {}", e), + ) + }), + } + } +} + +#[magnus::init] +fn init(ruby: &Ruby) -> Result<(), Error> { + let regorus_module = ruby.define_module("Regorus")?; + let engine_class = regorus_module.define_class("Engine", ruby.class_object())?; + + // ruby object methods + engine_class.define_alloc_func::(); + engine_class.define_method("initialize", method!(Engine::initialize, 0))?; + engine_class.define_method("clone", method!(Engine::clone, 0))?; + engine_class.define_method("<=>", method!(Engine::compare, 1))?; + // defines <, <=, >, >=, and == based on <=> + engine_class.include_module(module::comparable())?; + + // policy operations + engine_class.define_method("add_policy", method!(Engine::add_policy, 2))?; + engine_class.define_method( + "add_policy_from_file", + method!(Engine::add_policy_from_file, 1), + )?; + + // data operations + engine_class.define_method("add_data", method!(Engine::add_data, 1))?; + engine_class.define_method("add_data_json", method!(Engine::add_data_json, 1))?; + engine_class.define_method( + "add_data_from_json_file", + method!(Engine::add_data_from_json_file, 1), + )?; + engine_class.define_method("clear_data", method!(Engine::clear_data, 0))?; + + // input operations + engine_class.define_method("set_input", method!(Engine::set_input, 1))?; + engine_class.define_method("set_input_json", method!(Engine::set_input_json, 1))?; + engine_class.define_method( + "add_input_from_json_file", + method!(Engine::add_input_from_json_file, 1), + )?; + + // query operations + engine_class.define_method("eval_query", method!(Engine::eval_query, 1))?; + engine_class.define_method("eval_query_as_json", method!(Engine::eval_query_as_json, 1))?; + engine_class.define_method("eval_rule", method!(Engine::eval_rule, 1))?; + + Ok(()) +} diff --git a/bindings/ruby/lib/regorus.rb b/bindings/ruby/lib/regorus.rb new file mode 100644 index 00000000..f37a89cf --- /dev/null +++ b/bindings/ruby/lib/regorus.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +require_relative "regorus/version" +require_relative "regorusrb/regorusrb" + +module Regorus + class Engine; end +end diff --git a/bindings/ruby/lib/regorus/version.rb b/bindings/ruby/lib/regorus/version.rb new file mode 100644 index 00000000..4e639ff0 --- /dev/null +++ b/bindings/ruby/lib/regorus/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module Regorus + VERSION = "0.1.0" +end diff --git a/bindings/ruby/regorusrb.gemspec b/bindings/ruby/regorusrb.gemspec new file mode 100644 index 00000000..579e72b8 --- /dev/null +++ b/bindings/ruby/regorusrb.gemspec @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require_relative "lib/regorus/version" + +Gem::Specification.new do |spec| + spec.name = "regorusrb" + spec.version = Regorus::VERSION + spec.authors = ["David Marshall"] + + spec.summary = "Ruby bindings for Regorus - a fast, lightweight Rego interpreter written in Rust" + spec.homepage = "https://github.com/microsoft/regorus/blob/main/bindings/ruby" + spec.license = "MIT" + spec.required_ruby_version = ">= 3.0.0" + spec.required_rubygems_version = ">= 3.3.11" + + spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'" + + spec.metadata["homepage_uri"] = spec.homepage + spec.metadata["source_code_uri"] = spec.homepage + spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/bindings/ruby/CHANGELOG.md" + spec.metadata["rubygems_mfa_required"] = "true" + + # Specify which files should be added to the gem when it is released. + # The `git ls-files -z` loads the files in the RubyGem that have been added into git. + gemspec = File.basename(__FILE__) + spec.files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls| + ls.readlines("\x0", chomp: true).reject do |f| + (f == gemspec) || + f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile]) + end + end + + # Ensure Cargo.lock is included + spec.files << "../../Cargo.lock" if File.exist?("../../Cargo.lock") + + spec.bindir = "exe" + spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } + spec.require_paths = ["lib"] + spec.extensions = ["ext/regorusrb/Cargo.toml"] +end diff --git a/bindings/ruby/sig/regorusrb.rbs b/bindings/ruby/sig/regorusrb.rbs new file mode 100644 index 00000000..5a6406bb --- /dev/null +++ b/bindings/ruby/sig/regorusrb.rbs @@ -0,0 +1,4 @@ +module Regorus + VERSION: String + # See the writing guide of rbs: https://github.com/ruby/rbs#guides +end diff --git a/bindings/ruby/test/test_helper.rb b/bindings/ruby/test/test_helper.rb new file mode 100644 index 00000000..5b9aad71 --- /dev/null +++ b/bindings/ruby/test/test_helper.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +$LOAD_PATH.unshift File.expand_path("../lib", __dir__) +require "regorus" + +require "minitest/autorun" diff --git a/bindings/ruby/test/test_regorus.rb b/bindings/ruby/test/test_regorus.rb new file mode 100644 index 00000000..3bdf6b28 --- /dev/null +++ b/bindings/ruby/test/test_regorus.rb @@ -0,0 +1,180 @@ +# frozen_string_literal: true + +require "test_helper" +require "json" + +class TestRegorus < Minitest::Test + ALICE = "Alice" + BOB = "Bob" + CARLOS = "Carlos" + + def setup + @engine = ::Regorus::Engine.new + @engine.add_policy("regorus_test.rego", example_policy) + @engine.add_data(example_data) + end + + def example_policy + <<~REGO + package regorus_test + is_manager { + input.name == data.managers[_] + } + + is_employee { + input.name == data.employees[_] + } + + # Set a default value for to return false instead of nil + default is_manager_bool = false + default is_employee_bool = false + + is_manager_bool { + is_manager + } + + is_employee_bool { + is_employee + } + REGO + end + + def example_data + { + "managers" => [ALICE], + "employees" => [ALICE, BOB] + } + end + + def input_for(name) + { "name" => name } + end + + def test_version_number_presence + refute_nil ::Regorus::VERSION + end + + def test_engine_creation + assert_instance_of ::Regorus::Engine, ::Regorus::Engine.new + end + + def test_policy_addition + assert_silent { @engine.add_policy("example.rego", example_policy) } + end + + def test_object_creation_with_new + refute_same ::Regorus::Engine.new, ::Regorus::Engine.new + end + + def test_data_addition + assert_silent { @engine.add_data(example_data) } + end + + def test_data_addition_as_json + assert_silent { @engine.add_data_json(example_data.to_json) } + end + + def test_query_evaluation_for_alice + @engine.set_input(input_for(ALICE)) + + assert_equal alice_results, @engine.eval_query("data.regorus_test") + end + + def test_query_evaluation_for_bob + @engine.set_input(input_for(BOB)) + + assert_equal bob_results, @engine.eval_query("data.regorus_test") + end + + def test_query_evaluation_as_json + @engine.set_input(input_for(ALICE)) + + assert_equal alice_results.to_json, @engine.eval_query_as_json("data.regorus_test") + end + + def test_rule_evaluation_for_alice + @engine.set_input(input_for(ALICE)) + + assert @engine.eval_rule("data.regorus_test.is_employee") + assert @engine.eval_rule("data.regorus_test.is_employee_bool") + assert @engine.eval_rule("data.regorus_test.is_manager") + assert @engine.eval_rule("data.regorus_test.is_manager_bool") + end + + def test_rule_evaluation_for_bob + @engine.set_input(input_for(BOB)) + + assert @engine.eval_rule("data.regorus_test.is_employee") + assert @engine.eval_rule("data.regorus_test.is_employee_bool") + assert_nil @engine.eval_rule("data.regorus_test.is_manager") + refute @engine.eval_rule("data.regorus_test.is_manager_bool") + end + + def test_rule_evaluation_for_carlos + @engine.set_input(input_for(CARLOS)) + + assert_nil @engine.eval_rule("data.regorus_test.is_employee") + refute @engine.eval_rule("data.regorus_test.is_employee_bool") + assert_nil @engine.eval_rule("data.regorus_test.is_manager") + refute @engine.eval_rule("data.regorus_test.is_manager_bool") + end + + def test_missing_rules_handling + @engine.set_input(input_for(ALICE)) + assert_raises(RuntimeError) { @engine.eval_rule("data.regorus_test.not_a_rule") } + end + + def test_engine_cloning + cloned_engine = @engine.clone + + assert_instance_of ::Regorus::Engine, cloned_engine + refute_same @engine, cloned_engine + end + + def alice_results + { + result: [ + { + expressions: [ + { + value: { + "is_employee" => true, + "is_employee_bool" => true, + "is_manager" => true, + "is_manager_bool" => true + }, + text: "data.regorus_test", + location: { + row: 1, + col: 1 + } + } + ] + } + ] + } + end + + def bob_results + { + result: [ + { + expressions: [ + { + value: { + "is_employee" => true, + "is_employee_bool" => true, + "is_manager_bool" => false + }, + text: "data.regorus_test", + location: { + row: 1, + col: 1 + } + } + ] + } + ] + } + end +end