mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
Ruby bindings for existing FFI methods, plus eval_rule() (#188)
* Ruby binding * use relative regorus crate in ruby instead of a published version, also cargo fmt * remove unnecessary Cargo.toml, include the top level Cargo.lock in ruby gem * ruby bindings continued- add eval_rule, fix _json methods, update README.md also added rubocop-minitest and rubocop-rake, and added more test coverage * update README.md to include Ruby bindings Closes #191
This commit is contained in:
@@ -5,6 +5,7 @@ members = [
|
||||
"bindings/python",
|
||||
"bindings/wasm",
|
||||
"bindings/java",
|
||||
"bindings/ruby/ext/regorusrb",
|
||||
]
|
||||
|
||||
[package]
|
||||
|
||||
@@ -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.
|
||||
|
||||
14
bindings/ruby/.gitignore
vendored
Normal file
14
bindings/ruby/.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
/.bundle/
|
||||
/.yardoc
|
||||
/_yardoc/
|
||||
/coverage/
|
||||
/doc/
|
||||
/pkg/
|
||||
/spec/reports/
|
||||
/tmp/
|
||||
*.bundle
|
||||
*.so
|
||||
*.o
|
||||
*.a
|
||||
mkmf.log
|
||||
target/
|
||||
29
bindings/ruby/.rubocop.yml
Normal file
29
bindings/ruby/.rubocop.yml
Normal file
@@ -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
|
||||
1
bindings/ruby/.tool-versions
Normal file
1
bindings/ruby/.tool-versions
Normal file
@@ -0,0 +1 @@
|
||||
ruby 3.3.0
|
||||
5
bindings/ruby/CHANGELOG.md
Normal file
5
bindings/ruby/CHANGELOG.md
Normal file
@@ -0,0 +1,5 @@
|
||||
## [Unreleased]
|
||||
|
||||
## [0.1.0] - 2024-03-29
|
||||
|
||||
- Initial release
|
||||
16
bindings/ruby/Gemfile
Normal file
16
bindings/ruby/Gemfile
Normal file
@@ -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
|
||||
61
bindings/ruby/Gemfile.lock
Normal file
61
bindings/ruby/Gemfile.lock
Normal file
@@ -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
|
||||
1
bindings/ruby/LICENSE.txt
Symbolic link
1
bindings/ruby/LICENSE.txt
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE
|
||||
95
bindings/ruby/README.md
Normal file
95
bindings/ruby/README.md
Normal file
@@ -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).
|
||||
|
||||
23
bindings/ruby/Rakefile
Normal file
23
bindings/ruby/Rakefile
Normal file
@@ -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]
|
||||
11
bindings/ruby/bin/console
Executable file
11
bindings/ruby/bin/console
Executable file
@@ -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__)
|
||||
8
bindings/ruby/bin/setup
Executable file
8
bindings/ruby/bin/setup
Executable file
@@ -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
|
||||
16
bindings/ruby/ext/regorusrb/Cargo.toml
Normal file
16
bindings/ruby/ext/regorusrb/Cargo.toml
Normal file
@@ -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"
|
||||
6
bindings/ruby/ext/regorusrb/extconf.rb
Normal file
6
bindings/ruby/ext/regorusrb/extconf.rb
Normal file
@@ -0,0 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "mkmf"
|
||||
require "rb_sys/mkmf"
|
||||
|
||||
create_rust_makefile("regorusrb/regorusrb")
|
||||
224
bindings/ruby/ext/regorusrb/src/lib.rs
Normal file
224
bindings/ruby/ext/regorusrb/src/lib.rs
Normal file
@@ -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<RegorusEngine>,
|
||||
}
|
||||
|
||||
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<i32, Error> {
|
||||
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<magnus::Value, Error> {
|
||||
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<String, Error> {
|
||||
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<Option<magnus::Value>, 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>();
|
||||
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(())
|
||||
}
|
||||
8
bindings/ruby/lib/regorus.rb
Normal file
8
bindings/ruby/lib/regorus.rb
Normal file
@@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative "regorus/version"
|
||||
require_relative "regorusrb/regorusrb"
|
||||
|
||||
module Regorus
|
||||
class Engine; end
|
||||
end
|
||||
5
bindings/ruby/lib/regorus/version.rb
Normal file
5
bindings/ruby/lib/regorus/version.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Regorus
|
||||
VERSION = "0.1.0"
|
||||
end
|
||||
40
bindings/ruby/regorusrb.gemspec
Normal file
40
bindings/ruby/regorusrb.gemspec
Normal file
@@ -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
|
||||
4
bindings/ruby/sig/regorusrb.rbs
Normal file
4
bindings/ruby/sig/regorusrb.rbs
Normal file
@@ -0,0 +1,4 @@
|
||||
module Regorus
|
||||
VERSION: String
|
||||
# See the writing guide of rbs: https://github.com/ruby/rbs#guides
|
||||
end
|
||||
6
bindings/ruby/test/test_helper.rb
Normal file
6
bindings/ruby/test/test_helper.rb
Normal file
@@ -0,0 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
||||
require "regorus"
|
||||
|
||||
require "minitest/autorun"
|
||||
180
bindings/ruby/test/test_regorus.rb
Normal file
180
bindings/ruby/test/test_regorus.rb
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user