Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 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
3 changes: 3 additions & 0 deletions bindings/wasm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,6 @@ 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.
19 changes: 19 additions & 0 deletions bindings/wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,14 @@ 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.

#[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 +166,14 @@ impl Engine {
self.engine.add_data(data).map_err(error_to_jsvalue)
}

/// Prepare the engine for evaluation.
///
/// This initializes internal evaluation structures so a cloned engine can
/// evaluate without requiring an initial "dummy" evaluation.
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.

Do we need to remark that if we don't call this, the first eval on the engine will pay this cost. Also about adding policies after preparation.

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.

Added explicit prepare semantics in wasm docs: first eval pays this one-time setup if prepare() is not called, and modifying policy/data after prepare invalidates prepared state. Commit: 730e6de.

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 +503,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
28 changes: 28 additions & 0 deletions src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,34 @@ impl Engine {
self.add_data(Value::from_json_str(data_json)?)
}

/// Prepare the engine for evaluation without executing a query or rule.
///
/// This parses and initializes internal evaluation data structures so that
/// subsequent evaluations (or cloned engines) can run without paying the
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.

Can we make the documentation more clear about why this is needed, what ahappens if not called, what happens if policies added after this etc.

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.

Expanded core Engine::prepare() docs with why it exists, behavior when omitted (first eval prepares implicitly), and invalidation rules after policy/data changes. Commit: 730e6de.

/// one-time preparation cost during the first evaluation call.
///
/// ```
/// # 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