diff --git a/bindings/cpp/regorus.hpp b/bindings/cpp/regorus.hpp index 191e4cf21..554083d37 100644 --- a/bindings/cpp/regorus.hpp +++ b/bindings/cpp/regorus.hpp @@ -80,6 +80,10 @@ namespace regorus { return std::unique_ptr(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)); } diff --git a/bindings/csharp/Regorus/Engine.cs b/bindings/csharp/Regorus/Engine.cs index 852be9783..9d420b0d5 100644 --- a/bindings/csharp/Regorus/Engine.cs +++ b/bindings/csharp/Regorus/Engine.cs @@ -68,6 +68,18 @@ public Engine Clone() }); } + /// + /// Prepare internal evaluation structures without executing a query. + /// This is optional: if skipped, the first evaluation pays this setup cost. + /// + public void Prepare() + { + UseHandle(enginePtr => + { + CheckAndDropResult(Regorus.Internal.API.regorus_engine_prepare((Regorus.Internal.RegorusEngine*)enginePtr)); + }); + } + public void SetStrictBuiltinErrors(bool strict) { UseHandle(enginePtr => diff --git a/bindings/csharp/Regorus/NativeMethods.cs b/bindings/csharp/Regorus/NativeMethods.cs index c7a071b9b..a7339f3fe 100644 --- a/bindings/csharp/Regorus/NativeMethods.cs +++ b/bindings/csharp/Regorus/NativeMethods.cs @@ -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); + /// + /// Prepare a RegorusEngine for evaluation without executing a query. + /// + [DllImport(LibraryName, EntryPoint = "regorus_engine_prepare", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern RegorusResult regorus_engine_prepare(RegorusEngine* engine); + /// /// Compile an RVM program from the engine state with entry points. /// diff --git a/bindings/ffi/src/engine.rs b/bindings/ffi/src/engine.rs index 8079b8e81..8ef15d78f 100644 --- a/bindings/ffi/src/engine.rs +++ b/bindings/ffi/src/engine.rs @@ -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) { diff --git a/bindings/go/pkg/regorus/mod.go b/bindings/go/pkg/regorus/mod.go index 042f2b3b3..b75c4ab2f 100644 --- a/bindings/go/pkg/regorus/mod.go +++ b/bindings/go/pkg/regorus/mod.go @@ -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) diff --git a/bindings/java/com_microsoft_regorus_Engine.h b/bindings/java/com_microsoft_regorus_Engine.h index ec507880c..9eec7586d 100644 --- a/bindings/java/com_microsoft_regorus_Engine.h +++ b/bindings/java/com_microsoft_regorus_Engine.h @@ -23,6 +23,14 @@ JNIEXPORT jlong JNICALL Java_com_microsoft_regorus_Engine_nativeNewEngine JNIEXPORT jlong JNICALL Java_com_microsoft_regorus_Engine_nativeClone (JNIEnv *, jclass, jlong); +/* + * Class: com_microsoft_regorus_Engine + * Method: nativePrepare + * Signature: (J)V + */ +JNIEXPORT void JNICALL Java_com_microsoft_regorus_Engine_nativePrepare + (JNIEnv *, jclass, jlong); + /* * Class: com_microsoft_regorus_Engine * Method: nativeAddPolicy diff --git a/bindings/java/src/lib.rs b/bindings/java/src/lib.rs index 9bd643950..8db171ee4 100644 --- a/bindings/java/src/lib.rs +++ b/bindings/java/src/lib.rs @@ -27,13 +27,30 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeNewEngine( #[no_mangle] pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeClone( - _env: EnvUnowned, + env: EnvUnowned, _class: JClass, engine_ptr: jlong, ) -> jlong { - let engine = unsafe { &mut *(engine_ptr as *mut Engine) }; - let c = engine.clone(); - Box::into_raw(Box::new(c)) as jlong + let res = throw_err(env, |_env| { + let engine = unsafe { &mut *get_engine_ptr(engine_ptr)? }; + let c = engine.clone(); + Ok(Box::into_raw(Box::new(c)) as jlong) + }); + + res.unwrap_or_default() +} + +#[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 *get_engine_ptr(engine_ptr)? }; + engine.prepare()?; + Ok(()) + }); } #[no_mangle] @@ -437,6 +454,9 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeDestroyEngine( _class: JClass, engine_ptr: jlong, ) { + if engine_ptr == 0 { + return; + } unsafe { let _engine = Box::from_raw(engine_ptr as *mut Engine); } @@ -816,6 +836,13 @@ fn throw_err(mut env: EnvUnowned, f: impl FnOnce(&mut Env) -> Result) -> R } } +fn get_engine_ptr(engine_ptr: jlong) -> Result<*mut Engine> { + if engine_ptr == 0 { + return Err(anyhow::anyhow!("Engine is closed")); + } + Ok(engine_ptr as *mut Engine) +} + fn get_string_array(env: &mut Env, array: jobjectArray) -> Result> { if array.is_null() { return Ok(Vec::new()); diff --git a/bindings/java/src/main/java/com/microsoft/regorus/Engine.java b/bindings/java/src/main/java/com/microsoft/regorus/Engine.java index 765c1dec3..de42c400b 100644 --- a/bindings/java/src/main/java/com/microsoft/regorus/Engine.java +++ b/bindings/java/src/main/java/com/microsoft/regorus/Engine.java @@ -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); @@ -45,7 +46,7 @@ public class Engine implements AutoCloseable, Cloneable { // Pointer to Engine allocated on Rust's heap, all native methods works on // engine expects this pointer. It is free'd in `close` method. - private final long enginePtr; + private long enginePtr; /** * Creates a new Regorus Engine. @@ -63,7 +64,15 @@ public Engine() { * Efficiently clones an Engine. */ public Engine clone() { - return new Engine(nativeClone(enginePtr)); + return new Engine(nativeClone(requireOpen())); + } + + /** + * Prepares internal evaluation structures without executing a query. + * Optional: if skipped, first evaluation performs the same setup. + */ + public void prepare() { + nativePrepare(requireOpen()); } /** @@ -73,7 +82,7 @@ public Engine clone() { * */ public void setRegoV0(boolean enable) { - nativeSetRegoV0(enginePtr, enable); + nativeSetRegoV0(requireOpen(), enable); } /** @@ -85,7 +94,7 @@ public void setRegoV0(boolean enable) { * @return Rego package defined in the policy. */ public String addPolicy(String filename, String rego) { - return nativeAddPolicy(enginePtr, filename, rego); + return nativeAddPolicy(requireOpen(), filename, rego); } /** @@ -96,7 +105,7 @@ public String addPolicy(String filename, String rego) { * @return Rego package defined in the policy. */ public String addPolicyFromFile(String path) { - return nativeAddPolicyFromFile(enginePtr, path); + return nativeAddPolicyFromFile(requireOpen(), path); } /** @@ -105,7 +114,7 @@ public String addPolicyFromFile(String path) { * @return List of Rego packages as a JSON array of strings. */ public String getPackages() { - return nativeGetPackages(enginePtr); + return nativeGetPackages(requireOpen()); } /** @@ -114,14 +123,14 @@ public String getPackages() { * @return List of Rego policies as a JSON array of sources. */ public String getPolicies() { - return nativeGetPolicies(enginePtr); + return nativeGetPolicies(requireOpen()); } /** * Clears the data document. */ public void clearData() { - nativeClearData(enginePtr); + nativeClearData(requireOpen()); } /** @@ -143,7 +152,7 @@ public void clearData() { * @param data Inline data document. */ public void addDataJson(String data) throws RuntimeException { - nativeAddDataJson(enginePtr, data); + nativeAddDataJson(requireOpen(), data); } /** @@ -160,7 +169,7 @@ public void addDataJson(String data) throws RuntimeException { * @param path Path to JSON data document. */ public void addDataJsonFromFile(String path) throws RuntimeException { - nativeAddDataJsonFromFile(enginePtr, path); + nativeAddDataJsonFromFile(requireOpen(), path); } /** @@ -169,7 +178,7 @@ public void addDataJsonFromFile(String path) throws RuntimeException { * @param input inline JSON input. */ public void setInputJson(String input) { - nativeSetInputJson(enginePtr, input); + nativeSetInputJson(requireOpen(), input); } /** @@ -178,7 +187,7 @@ public void setInputJson(String input) { * @param path Path to JSON input. */ public void setInputJsonFromFile(String path) { - nativeSetInputJsonFromFile(enginePtr, path); + nativeSetInputJsonFromFile(requireOpen(), path); } /** @@ -189,7 +198,7 @@ public void setInputJsonFromFile(String path) { * @return Query results as a JSON string. */ public String evalQuery(String query) { - return nativeEvalQuery(enginePtr, query); + return nativeEvalQuery(requireOpen(), query); } /** @@ -200,7 +209,7 @@ public String evalQuery(String query) { * @return Value of the rule as a JSON string. */ public String evalRule(String rule) { - return nativeEvalRule(enginePtr, rule); + return nativeEvalRule(requireOpen(), rule); } /** @@ -210,7 +219,7 @@ public String evalRule(String rule) { * */ public void setEnableCoverage(boolean enable) { - nativeSetEnableCoverage(enginePtr, enable); + nativeSetEnableCoverage(requireOpen(), enable); } /** @@ -218,7 +227,7 @@ public void setEnableCoverage(boolean enable) { * */ public void clearCoverageData() { - nativeClearCoverageData(enginePtr); + nativeClearCoverageData(requireOpen()); } /** @@ -228,7 +237,7 @@ public void clearCoverageData() { * */ public String getCoverageReport() { - return nativeGetCoverageReport(enginePtr); + return nativeGetCoverageReport(requireOpen()); } /** @@ -238,7 +247,7 @@ public String getCoverageReport() { * */ public String getCoverageReportPretty() { - return nativeGetCoverageReportPretty(enginePtr); + return nativeGetCoverageReportPretty(requireOpen()); } /** @@ -248,7 +257,7 @@ public String getCoverageReportPretty() { * */ public void setGatherPrints(boolean b) { - nativeSetGatherPrints(enginePtr, b); + nativeSetGatherPrints(requireOpen(), b); } /** @@ -258,7 +267,7 @@ public void setGatherPrints(boolean b) { * */ public String takePrints() { - return nativeTakePrints(enginePtr); + return nativeTakePrints(requireOpen()); } /** @@ -267,24 +276,34 @@ public String takePrints() { * @param config Policy length configuration. */ public void setPolicyLengthConfig(PolicyLengthConfig config) { - nativeSetPolicyLengthConfig(enginePtr, config.maxCol, config.maxFileBytes, config.maxLines); + nativeSetPolicyLengthConfig(requireOpen(), config.maxCol, config.maxFileBytes, config.maxLines); } /** * Clear the policy length configuration, reverting to defaults. */ public void clearPolicyLengthConfig() { - nativeClearPolicyLengthConfig(enginePtr); + nativeClearPolicyLengthConfig(requireOpen()); } long getPtr() { + return requireOpen(); + } + + private long requireOpen() { + if (enginePtr == 0) { + throw new IllegalStateException("Engine is closed"); + } return enginePtr; } @Override public void close() { - nativeDestroyEngine(enginePtr); + if (enginePtr != 0) { + nativeDestroyEngine(enginePtr); + enginePtr = 0; + } } // Loading native library from JAR is adapted from: diff --git a/bindings/java/src/test/java/com/microsoft/regorus/EngineTest.java b/bindings/java/src/test/java/com/microsoft/regorus/EngineTest.java index ae6f7dcb9..8c741e65a 100644 --- a/bindings/java/src/test/java/com/microsoft/regorus/EngineTest.java +++ b/bindings/java/src/test/java/com/microsoft/regorus/EngineTest.java @@ -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(); @@ -33,4 +44,28 @@ public void test_engine() Map expression = (Map) expressions.get(0); Assert.assertEquals("Hello, World!", expression.get("value")); } + + public void test_closed_engine_operations_throw() + { + Engine engine = new Engine(); + engine.close(); + + try { + engine.prepare(); + fail("prepare should fail on closed engine"); + } catch (IllegalStateException expected) { + } + + try { + engine.clone(); + fail("clone should fail on closed engine"); + } catch (IllegalStateException expected) { + } + + try { + engine.evalQuery("data"); + fail("evalQuery should fail on closed engine"); + } catch (IllegalStateException expected) { + } + } } diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs index 9943c881c..456df9013 100644 --- a/bindings/python/src/lib.rs +++ b/bindings/python/src/lib.rs @@ -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 diff --git a/bindings/python/test.py b/bindings/python/test.py index 5ef1beff1..55ecf2f0c 100644 --- a/bindings/python/test.py +++ b/bindings/python/test.py @@ -87,6 +87,7 @@ print(report) # Clone engine +engine.prepare() engine1 = engine.clone() diff --git a/bindings/ruby/ext/regorusrb/src/lib.rs b/bindings/ruby/ext/regorusrb/src/lib.rs index c1746e43b..544d52f71 100644 --- a/bindings/ruby/ext/regorusrb/src/lib.rs +++ b/bindings/ruby/ext/regorusrb/src/lib.rs @@ -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, Error> { self.engine .borrow() @@ -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))?; diff --git a/bindings/ruby/test/test_regorus.rb b/bindings/ruby/test/test_regorus.rb index b3ea418a0..3ba1860b1 100644 --- a/bindings/ruby/test/test_regorus.rb +++ b/bindings/ruby/test/test_regorus.rb @@ -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 diff --git a/bindings/wasm/README.md b/bindings/wasm/README.md index 36aa33c39..ea3caee8c 100644 --- a/bindings/wasm/README.md +++ b/bindings/wasm/README.md @@ -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. diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs index 034601720..9db59c0ee 100644 --- a/bindings/wasm/src/lib.rs +++ b/bindings/wasm/src/lib.rs @@ -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. + /// + /// 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. @@ -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 @@ -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)?; diff --git a/bindings/wasm/test.js b/bindings/wasm/test.js index 04d59bca0..3e11c8f46 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 bb0a70bf5..47b6e82e6 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -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) + } + /// Set whether builtins should raise errors strictly or not. /// /// Regorus differs from OPA in that by default builtins will @@ -1084,9 +1125,10 @@ impl Engine { limits::enforce_memory_limit().map_err(|err| anyhow!(err))?; self.interpreter.set_traces(enable_tracing); + let newly_prepared = !self.prepared; // if the data/policies have changed or the interpreter has never been prepared - if !self.prepared { + if newly_prepared { // Analyze the modules and determine how statements must be scheduled. let analyzer = Analyzer::new(); let schedule = Rc::new(analyzer.analyze(&self.modules)?); @@ -1116,23 +1158,28 @@ impl Engine { // Set schedule after hoisting completes self.interpreter.set_schedule(Some(schedule)); + } - #[cfg(feature = "azure_policy")] + #[cfg(feature = "azure_policy")] + { if for_target { - // Resolve and validate target specifications across all modules + // Resolve and validate target specifications across all modules. + // This must run for target-aware compilation even if generic prepare() + // was already called. crate::interpreter::target::resolve::resolve_and_apply_target( &mut self.interpreter, )?; // Infer resource types crate::interpreter::target::infer::infer_resource_type(&mut self.interpreter)?; - } - - if !for_target { - // Check if any module specifies a target and warn if so - #[cfg(feature = "azure_policy")] + } else if newly_prepared { + // Check if any module specifies a target and warn if so. self.warn_if_targets_present(); } + } + #[cfg(not(feature = "azure_policy"))] + let _ = for_target; + if newly_prepared { self.prepared = true; } diff --git a/tests/engine/mod.rs b/tests/engine/mod.rs index 292bcb9b9..a81b474a8 100644 --- a/tests/engine/mod.rs +++ b/tests/engine/mod.rs @@ -102,6 +102,135 @@ 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")))] +fn prepare_then_compile_for_target() -> Result<()> { + if !registry::targets::contains("target.tests.sample_test_target") { + let target = Target::from_json_str(include_str!( + "../interpreter/cases/target/definitions/sample_target.json" + ))?; + registry::targets::register(Rc::new(target))?; + } + + let mut engine = Engine::new(); + engine.add_policy( + "test.rego".to_string(), + r#"package test + import rego.v1 + __target__ := "target.tests.sample_test_target" + + default allow := false + + allow if { + input.type == "test_resource" + } + "# + .to_string(), + )?; + + engine.prepare()?; + let compiled = engine.compile_for_target()?; + let info = compiled.get_policy_info()?; + assert_eq!( + info.target_name.as_deref(), + Some("target.tests.sample_test_target") + ); + + let result = compiled.eval_with_input(Value::from_json_str( + r#"{"name":"resource-1","type":"test_resource"}"#, + )?)?; + assert_eq!(result, Value::from(true)); + + Ok(()) +} + +#[test] +#[cfg(feature = "azure_policy")] +#[cfg_attr(docsrs, doc(cfg(feature = "azure_policy")))] +fn prepare_then_compile_for_target_error_recovery() -> Result<()> { + let target_name = "target.tests.prepare_recovery_test_target"; + + let mut engine = Engine::new(); + engine.add_policy( + "test.rego".to_string(), + format!( + r#"package test + import rego.v1 + __target__ := "{target_name}" + + default allow := false + + allow if {{ + input.type == "test_resource" + }} + "# + ), + )?; + + engine.prepare()?; + assert!(engine.compile_for_target().is_err()); + + if !registry::targets::contains(target_name) { + let target_json = + include_str!("../interpreter/cases/target/definitions/sample_target.json") + .replace("target.tests.sample_test_target", target_name); + let target = Target::from_json_str(&target_json)?; + registry::targets::register(Rc::new(target))?; + } + + let compiled = engine.compile_for_target()?; + let info = compiled.get_policy_info()?; + assert_eq!(info.target_name.as_deref(), Some(target_name)); + + let result = compiled.eval_with_input(Value::from_json_str( + r#"{"name":"resource-1","type":"test_resource"}"#, + )?)?; + assert_eq!(result, Value::from(true)); + + Ok(()) +} + #[test] #[cfg(feature = "azure_policy")] #[cfg_attr(docsrs, doc(cfg(feature = "azure_policy")))]