Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions bindings/cpp/regorus.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ namespace regorus {
return std::unique_ptr<Engine>(new Engine(regorus_engine_clone(engine)));
}

Result prepare() {
return Result(regorus_engine_prepare(engine));
}

Result set_rego_v0(bool enable) {
return Result(regorus_engine_set_rego_v0(engine, enable));
}
Expand Down
12 changes: 12 additions & 0 deletions bindings/csharp/Regorus/Engine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,18 @@ public Engine Clone()
});
}

/// <summary>
/// Prepare internal evaluation structures without executing a query.
/// This is optional: if skipped, the first evaluation pays this setup cost.
/// </summary>
public void Prepare()
{
UseHandle(enginePtr =>
{
CheckAndDropResult(Regorus.Internal.API.regorus_engine_prepare((Regorus.Internal.RegorusEngine*)enginePtr));
});
}

public void SetStrictBuiltinErrors(bool strict)
{
UseHandle(enginePtr =>
Expand Down
6 changes: 6 additions & 0 deletions bindings/csharp/Regorus/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ internal static unsafe partial class API
[DllImport(LibraryName, EntryPoint = "regorus_engine_clone", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusEngine* regorus_engine_clone(RegorusEngine* engine);

/// <summary>
/// Prepare a RegorusEngine for evaluation without executing a query.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_engine_prepare", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_prepare(RegorusEngine* engine);

/// <summary>
/// Compile an RVM program from the engine state with entry points.
/// </summary>
Expand Down
15 changes: 15 additions & 0 deletions bindings/ffi/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,21 @@ pub extern "C" fn regorus_engine_clone(engine: *mut RegorusEngine) -> *mut Regor
}
}

/// Prepare a [`RegorusEngine`] for evaluation without executing a query.
///
/// This is optional. If not called, first eval performs the same setup.
/// If policy/data changes after preparation, setup is invalidated.
#[no_mangle]
pub extern "C" fn regorus_engine_prepare(engine: *mut RegorusEngine) -> RegorusResult {
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.prepare()
}())
})
}

#[no_mangle]
pub extern "C" fn regorus_engine_drop(engine: *mut RegorusEngine) {
if let Ok(e) = to_ref(engine) {
Expand Down
11 changes: 11 additions & 0 deletions bindings/go/pkg/regorus/mod.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ func (e *Engine) Clone() *Engine {
return c
}

func (e *Engine) Prepare() error {
result := C.regorus_engine_prepare(e.e)
defer C.regorus_result_drop(result)

if result.status != C.Ok {
return fmt.Errorf("%s", C.GoString(result.error_message))
}

return nil
}

func (e *Engine) SetRegoV0(enable bool) error {
result := C.regorus_engine_set_rego_v0(e.e, C.bool(enable))
defer C.regorus_result_drop(result)
Expand Down
8 changes: 8 additions & 0 deletions bindings/java/com_microsoft_regorus_Engine.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions bindings/java/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,19 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeClone(
Box::into_raw(Box::new(c)) as jlong
}

#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativePrepare(
env: EnvUnowned,
_class: JClass,
engine_ptr: jlong,
) {
let _ = throw_err(env, |_env| {
let engine = unsafe { &mut *(engine_ptr as *mut Engine) };
engine.prepare()?;
Ok(())
});
}

#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeSetRegoV0(
env: EnvUnowned,
Expand Down
9 changes: 9 additions & 0 deletions bindings/java/src/main/java/com/microsoft/regorus/Engine.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public class Engine implements AutoCloseable, Cloneable {
// if you update the native API.
private static native long nativeNewEngine();
private static native long nativeClone(long enginePtr);
private static native void nativePrepare(long enginePtr);
private static native void nativeSetRegoV0(long enginePtr, boolean enable);
private static native String nativeAddPolicy(long enginePtr, String path, String rego);
private static native String nativeAddPolicyFromFile(long enginePtr, String path);
Expand Down Expand Up @@ -65,6 +66,14 @@ public Engine() {
public Engine clone() {
return new Engine(nativeClone(enginePtr));
}

/**
* Prepares internal evaluation structures without executing a query.
* Optional: if skipped, first evaluation performs the same setup.
*/
public void prepare() {
nativePrepare(enginePtr);
}

/**
* Enable/disable Rego v0.
Expand Down
11 changes: 11 additions & 0 deletions bindings/java/src/test/java/com/microsoft/regorus/EngineTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,19 @@ public void test_engine()
"package test\nmessage = concat(\", \", [input.message, data.message])"
);
engine.addDataJson("{\"message\":\"World!\"}");
engine.prepare();
engine.setInputJson("{\"message\":\"Hello\"}");
resJson = engine.evalQuery("data.test.message");

try (Engine template = engine.clone()) {
template.setInputJson("{\"message\":\"Hi\"}");
String templateResJson = template.evalQuery("data.test.message");
Map templateRes = new Gson().fromJson(templateResJson, Map.class);
ArrayList templateResults = (ArrayList) templateRes.get("result");
ArrayList templateExpressions = (ArrayList) ((Map) templateResults.get(0)).get("expressions");
Map templateExpression = (Map) templateExpressions.get(0);
Assert.assertEquals("Hi, World!", templateExpression.get("value"));
}
}

Gson gson = new Gson();
Expand Down
7 changes: 7 additions & 0 deletions bindings/python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,13 @@ impl Engine {
self.engine.take_prints()
}

/// Prepare internal evaluation structures without executing a query.
///
/// Optional: if skipped, first evaluation performs the same setup.
pub fn prepare(&mut self) -> Result<()> {
self.engine.prepare()
}

/// Clone a [`Engine`]
///
/// To avoid having to parse same policy again, the engine can be cloned
Expand Down
1 change: 1 addition & 0 deletions bindings/python/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
print(report)

# Clone engine
engine.prepare()
engine1 = engine.clone()


Expand Down
8 changes: 8 additions & 0 deletions bindings/ruby/ext/regorusrb/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,13 @@ impl Engine {
Ok(())
}

fn prepare(&self) -> Result<(), Error> {
self.engine
.borrow_mut()
.prepare()
.map_err(|e| Error::new(runtime_error(), format!("Failed to prepare engine: {e}")))
}

fn get_packages(&self) -> Result<Vec<String>, Error> {
self.engine
.borrow()
Expand Down Expand Up @@ -373,6 +380,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
method!(Engine::add_data_from_json_file, 1),
)?;
engine_class.define_method("clear_data", method!(Engine::clear_data, 0))?;
engine_class.define_method("prepare", method!(Engine::prepare, 0))?;

// input operations
engine_class.define_method("set_input", method!(Engine::set_input, 1))?;
Expand Down
1 change: 1 addition & 0 deletions bindings/ruby/test/test_regorus.rb
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ def test_missing_rules_handling
end

def test_engine_cloning
@engine.prepare
cloned_engine = @engine.clone

assert_instance_of ::Regorus::Engine, cloned_engine
Expand Down
6 changes: 6 additions & 0 deletions bindings/wasm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,9 @@ Run `cargo xtask build-wasm` to invoke wasm-pack with sensible defaults, or `car
## Usage

See [test.js](https://github.com/microsoft/regorus/blob/main/bindings/wasm/test.js) for example usage.

For best performance with large policies, call `engine.prepare()` after loading
policy/data, then use `engine.clone()` to create per-request engines. If
`prepare()` is skipped, the first `eval*` call performs the same one-time
setup. Adding/changing policy or data after `prepare()` invalidates the
prepared state.
28 changes: 28 additions & 0 deletions bindings/wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,17 @@ impl Engine {
self.engine.set_rego_v0(enable)
}

/// Clone this engine.
///
/// Useful for creating per-request engines after loading policy/data once.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remarks on the performance of this. Tahe a deep look. It is intended to avoid deep colies.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the clone docs to clarify performance intent: clone avoids reparsing/reloading immutable policy structures, while mutable evaluation state is copied for isolation. Commit: 730e6de.

///
/// Clone is designed to avoid reparsing policy text and reloading immutable
/// policy structures. Mutable evaluation state is copied for isolation.
#[wasm_bindgen(js_name = "clone")]
pub fn cloneEngine(&self) -> Engine {
Clone::clone(self)
}

/// Add a policy
///
/// The policy is parsed into AST.
Expand All @@ -158,6 +169,20 @@ impl Engine {
self.engine.add_data(data).map_err(error_to_jsvalue)
}

/// Prepare the engine for evaluation.
///
/// The first evaluation on an unprepared engine performs one-time setup.
/// Calling `prepare()` performs that setup eagerly.
///
/// This is optional for correctness. If omitted, the first `eval*` call
/// implicitly performs preparation.
///
/// If policies/data are modified after `prepare()`, preparation is
/// invalidated and must be performed again (explicitly or via first eval).
pub fn prepare(&mut self) -> Result<(), JsValue> {
self.engine.prepare().map_err(error_to_jsvalue)
}

/// Get the list of packages defined by loaded policies.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.get_packages
Expand Down Expand Up @@ -487,6 +512,9 @@ mod tests {
)?;
assert_eq!(pkg, "data.test");

// Prepare before first evaluation.
engine.prepare()?;

let results = engine.evalQuery("data".to_string())?;
let r = regorus::Value::from_json_str(&results).map_err(error_to_jsvalue)?;

Expand Down
7 changes: 7 additions & 0 deletions bindings/wasm/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ engine.addDataJson(`
}
`);

// Prepare internal evaluation structures once.
engine.prepare();

// Clone a prepared template engine for reuse.
var template = engine.clone();
engine = template.clone();

// Set policy input
engine.setInputJson(`
{
Expand Down
41 changes: 41 additions & 0 deletions src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,47 @@ impl Engine {
self.add_data(Value::from_json_str(data_json)?)
}

/// Prepare the engine for evaluation without executing a query or rule.
///
/// The first evaluation on an unprepared engine performs one-time setup
/// (analysis, scheduling, imports/rules processing, and initialization of
/// internal evaluation structures). Calling this method performs that work
/// eagerly so a later call to [`Engine::eval_rule`] / [`Engine::eval_query`]
/// does not pay that startup cost.
///
/// This method is optional for correctness. If omitted, the first
/// evaluation will implicitly prepare the engine.
///
/// Preparation is invalidated when policy/data that affects evaluation is
/// changed (for example: [`Engine::add_policy`], [`Engine::add_policy_from_file`],
/// [`Engine::add_data`], [`Engine::clear_data`]). In those cases, the next
/// evaluation (or another explicit call to `prepare`) performs setup again.
///
/// This is especially useful before cloning template engines used for
/// repeated evaluations.
///
/// ```
/// # use regorus::*;
/// # fn main() -> anyhow::Result<()> {
/// let mut engine = Engine::new();
/// engine.add_policy("test.rego".to_string(), r#"
/// package test
/// import rego.v1
/// allow if input.user == "alice"
/// "#.to_string())?;
///
/// engine.prepare()?;
/// let mut cloned = engine.clone();
///
/// cloned.set_input_json(r#"{"user":"alice"}"#)?;
/// assert_eq!(cloned.eval_rule("data.test.allow".to_string())?, Value::from(true));
/// # Ok(())
/// # }
/// ```
pub fn prepare(&mut self) -> Result<()> {
self.prepare_for_eval(false, false)
}
Comment on lines +545 to +547

/// Set whether builtins should raise errors strictly or not.
///
/// Regorus differs from OPA in that by default builtins will
Expand Down
40 changes: 40 additions & 0 deletions tests/engine/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,46 @@ fn extension_with_state() -> Result<()> {
Ok(())
}

#[test]
fn prepare_then_clone_without_initial_eval() -> Result<()> {
let mut engine = Engine::new();
engine.add_policy(
"test.rego".to_string(),
r#"package test
import rego.v1

default allow := false

allow if {
input.user in data.allowed_users
}
"#
.to_string(),
)?;
engine.add_data(Value::from_json_str(
r#"{"allowed_users":["alice","bob"]}"#,
)?)?;

// Prepare once and clone without running an initial evaluation.
engine.prepare()?;

let mut alice_engine = engine.clone();
alice_engine.set_input_json(r#"{"user":"alice"}"#)?;
assert_eq!(
alice_engine.eval_rule("data.test.allow".to_string())?,
Value::from(true)
);

let mut mallory_engine = engine.clone();
mallory_engine.set_input_json(r#"{"user":"mallory"}"#)?;
assert_eq!(
mallory_engine.eval_rule("data.test.allow".to_string())?,
Value::from(false)
);

Ok(())
}

#[test]
#[cfg(feature = "azure_policy")]
#[cfg_attr(docsrs, doc(cfg(feature = "azure_policy")))]
Expand Down