From 72515f6d4cb4224cc599811da95c8ab016f119cf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 20:16:02 +0000 Subject: [PATCH] Expose wasm engine clone and add prepare API Agent-Logs-Url: https://github.com/microsoft/regorus/sessions/58419b20-1586-4a23-85fb-5aed5f5d5961 Co-authored-by: anakrish <35780660+anakrish@users.noreply.github.com> --- bindings/wasm/README.md | 3 +++ bindings/wasm/src/lib.rs | 19 +++++++++++++++++++ bindings/wasm/test.js | 7 +++++++ src/engine.rs | 28 ++++++++++++++++++++++++++++ tests/engine/mod.rs | 40 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 97 insertions(+) diff --git a/bindings/wasm/README.md b/bindings/wasm/README.md index 36aa33c..6b08160 100644 --- a/bindings/wasm/README.md +++ b/bindings/wasm/README.md @@ -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. diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs index 0346017..7b937b4 100644 --- a/bindings/wasm/src/lib.rs +++ b/bindings/wasm/src/lib.rs @@ -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. + #[wasm_bindgen(js_name = "clone")] + pub fn cloneEngine(&self) -> Engine { + Clone::clone(self) + } + /// Add a policy /// /// The policy is parsed into AST. @@ -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. + 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 @@ -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)?; diff --git a/bindings/wasm/test.js b/bindings/wasm/test.js index 04d59bc..3e11c8f 100644 --- a/bindings/wasm/test.js +++ b/bindings/wasm/test.js @@ -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(` { diff --git a/src/engine.rs b/src/engine.rs index bb0a70b..831b3eb 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -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 + /// 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) + } + /// Set whether builtins should raise errors strictly or not. /// /// Regorus differs from OPA in that by default builtins will diff --git a/tests/engine/mod.rs b/tests/engine/mod.rs index 292bcb9..6c6090d 100644 --- a/tests/engine/mod.rs +++ b/tests/engine/mod.rs @@ -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")))]