Compare commits

...

7 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
639bfe3246 Fix deep-review findings for prepare/closed-handle safety
Agent-Logs-Url: https://github.com/microsoft/regorus/sessions/c2c0f3ef-f641-4d83-94f8-a3cd3ede6db8

Co-authored-by: anakrish <35780660+anakrish@users.noreply.github.com>
2026-05-15 12:04:40 +00:00
copilot-swe-agent[bot]
4cc82e2fda Address review nits after validation feedback
Agent-Logs-Url: https://github.com/microsoft/regorus/sessions/f964122b-5af3-41a7-bc51-d87b7271a455

Co-authored-by: anakrish <35780660+anakrish@users.noreply.github.com>
2026-05-14 20:11:51 +00:00
copilot-swe-agent[bot]
196b6d68aa Fix target-aware prepare path and add regression test
Agent-Logs-Url: https://github.com/microsoft/regorus/sessions/f964122b-5af3-41a7-bc51-d87b7271a455

Co-authored-by: anakrish <35780660+anakrish@users.noreply.github.com>
2026-05-14 20:07:11 +00:00
copilot-swe-agent[bot]
c65e844f63 Strengthen Java clone test coverage
Agent-Logs-Url: https://github.com/microsoft/regorus/sessions/49ba4462-95d3-42c0-a302-db1b81df4f65

Co-authored-by: anakrish <35780660+anakrish@users.noreply.github.com>
2026-05-14 13:18:27 +00:00
copilot-swe-agent[bot]
730e6de75a Extend prepare API/docs across bindings
Agent-Logs-Url: https://github.com/microsoft/regorus/sessions/49ba4462-95d3-42c0-a302-db1b81df4f65

Co-authored-by: anakrish <35780660+anakrish@users.noreply.github.com>
2026-05-14 13:13:43 +00:00
copilot-swe-agent[bot]
72515f6d4c 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>
2026-05-13 20:16:02 +00:00
copilot-swe-agent[bot]
839933c933 Initial plan 2026-05-13 20:10:08 +00:00
18 changed files with 406 additions and 35 deletions

View File

@@ -80,6 +80,10 @@ namespace regorus {
return std::unique_ptr<Engine>(new Engine(regorus_engine_clone(engine))); 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) { Result set_rego_v0(bool enable) {
return Result(regorus_engine_set_rego_v0(engine, enable)); return Result(regorus_engine_set_rego_v0(engine, enable));
} }

View File

@@ -68,6 +68,18 @@ namespace Regorus
}); });
} }
/// <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) public void SetStrictBuiltinErrors(bool strict)
{ {
UseHandle(enginePtr => UseHandle(enginePtr =>

View File

@@ -92,6 +92,12 @@ namespace Regorus.Internal
[DllImport(LibraryName, EntryPoint = "regorus_engine_clone", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] [DllImport(LibraryName, EntryPoint = "regorus_engine_clone", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusEngine* regorus_engine_clone(RegorusEngine* engine); 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> /// <summary>
/// Compile an RVM program from the engine state with entry points. /// Compile an RVM program from the engine state with entry points.
/// </summary> /// </summary>

View File

@@ -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] #[no_mangle]
pub extern "C" fn regorus_engine_drop(engine: *mut RegorusEngine) { pub extern "C" fn regorus_engine_drop(engine: *mut RegorusEngine) {
if let Ok(e) = to_ref(engine) { if let Ok(e) = to_ref(engine) {

View File

@@ -28,6 +28,17 @@ func (e *Engine) Clone() *Engine {
return c 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 { func (e *Engine) SetRegoV0(enable bool) error {
result := C.regorus_engine_set_rego_v0(e.e, C.bool(enable)) result := C.regorus_engine_set_rego_v0(e.e, C.bool(enable))
defer C.regorus_result_drop(result) defer C.regorus_result_drop(result)

View File

@@ -23,6 +23,14 @@ JNIEXPORT jlong JNICALL Java_com_microsoft_regorus_Engine_nativeNewEngine
JNIEXPORT jlong JNICALL Java_com_microsoft_regorus_Engine_nativeClone JNIEXPORT jlong JNICALL Java_com_microsoft_regorus_Engine_nativeClone
(JNIEnv *, jclass, jlong); (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 * Class: com_microsoft_regorus_Engine
* Method: nativeAddPolicy * Method: nativeAddPolicy

View File

@@ -27,13 +27,30 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeNewEngine(
#[no_mangle] #[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeClone( pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeClone(
_env: EnvUnowned, env: EnvUnowned,
_class: JClass, _class: JClass,
engine_ptr: jlong, engine_ptr: jlong,
) -> jlong { ) -> jlong {
let engine = unsafe { &mut *(engine_ptr as *mut Engine) }; let res = throw_err(env, |_env| {
let c = engine.clone(); let engine = unsafe { &mut *get_engine_ptr(engine_ptr)? };
Box::into_raw(Box::new(c)) as jlong 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] #[no_mangle]
@@ -437,6 +454,9 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeDestroyEngine(
_class: JClass, _class: JClass,
engine_ptr: jlong, engine_ptr: jlong,
) { ) {
if engine_ptr == 0 {
return;
}
unsafe { unsafe {
let _engine = Box::from_raw(engine_ptr as *mut Engine); let _engine = Box::from_raw(engine_ptr as *mut Engine);
} }
@@ -816,6 +836,13 @@ fn throw_err<T>(mut env: EnvUnowned, f: impl FnOnce(&mut Env) -> Result<T>) -> 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<Vec<String>> { fn get_string_array(env: &mut Env, array: jobjectArray) -> Result<Vec<String>> {
if array.is_null() { if array.is_null() {
return Ok(Vec::new()); return Ok(Vec::new());

View File

@@ -21,6 +21,7 @@ public class Engine implements AutoCloseable, Cloneable {
// if you update the native API. // if you update the native API.
private static native long nativeNewEngine(); private static native long nativeNewEngine();
private static native long nativeClone(long enginePtr); 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 void nativeSetRegoV0(long enginePtr, boolean enable);
private static native String nativeAddPolicy(long enginePtr, String path, String rego); private static native String nativeAddPolicy(long enginePtr, String path, String rego);
private static native String nativeAddPolicyFromFile(long enginePtr, String path); 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 // Pointer to Engine allocated on Rust's heap, all native methods works on
// engine expects this pointer. It is free'd in `close` method. // engine expects this pointer. It is free'd in `close` method.
private final long enginePtr; private long enginePtr;
/** /**
* Creates a new Regorus Engine. * Creates a new Regorus Engine.
@@ -63,7 +64,15 @@ public class Engine implements AutoCloseable, Cloneable {
* Efficiently clones an Engine. * Efficiently clones an Engine.
*/ */
public Engine clone() { 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 class Engine implements AutoCloseable, Cloneable {
* *
*/ */
public void setRegoV0(boolean enable) { public void setRegoV0(boolean enable) {
nativeSetRegoV0(enginePtr, enable); nativeSetRegoV0(requireOpen(), enable);
} }
/** /**
@@ -85,7 +94,7 @@ public class Engine implements AutoCloseable, Cloneable {
* @return Rego package defined in the policy. * @return Rego package defined in the policy.
*/ */
public String addPolicy(String filename, String rego) { public String addPolicy(String filename, String rego) {
return nativeAddPolicy(enginePtr, filename, rego); return nativeAddPolicy(requireOpen(), filename, rego);
} }
/** /**
@@ -96,7 +105,7 @@ public class Engine implements AutoCloseable, Cloneable {
* @return Rego package defined in the policy. * @return Rego package defined in the policy.
*/ */
public String addPolicyFromFile(String path) { public String addPolicyFromFile(String path) {
return nativeAddPolicyFromFile(enginePtr, path); return nativeAddPolicyFromFile(requireOpen(), path);
} }
/** /**
@@ -105,7 +114,7 @@ public class Engine implements AutoCloseable, Cloneable {
* @return List of Rego packages as a JSON array of strings. * @return List of Rego packages as a JSON array of strings.
*/ */
public String getPackages() { public String getPackages() {
return nativeGetPackages(enginePtr); return nativeGetPackages(requireOpen());
} }
/** /**
@@ -114,14 +123,14 @@ public class Engine implements AutoCloseable, Cloneable {
* @return List of Rego policies as a JSON array of sources. * @return List of Rego policies as a JSON array of sources.
*/ */
public String getPolicies() { public String getPolicies() {
return nativeGetPolicies(enginePtr); return nativeGetPolicies(requireOpen());
} }
/** /**
* Clears the data document. * Clears the data document.
*/ */
public void clearData() { public void clearData() {
nativeClearData(enginePtr); nativeClearData(requireOpen());
} }
/** /**
@@ -143,7 +152,7 @@ public class Engine implements AutoCloseable, Cloneable {
* @param data Inline data document. * @param data Inline data document.
*/ */
public void addDataJson(String data) throws RuntimeException { public void addDataJson(String data) throws RuntimeException {
nativeAddDataJson(enginePtr, data); nativeAddDataJson(requireOpen(), data);
} }
/** /**
@@ -160,7 +169,7 @@ public class Engine implements AutoCloseable, Cloneable {
* @param path Path to JSON data document. * @param path Path to JSON data document.
*/ */
public void addDataJsonFromFile(String path) throws RuntimeException { public void addDataJsonFromFile(String path) throws RuntimeException {
nativeAddDataJsonFromFile(enginePtr, path); nativeAddDataJsonFromFile(requireOpen(), path);
} }
/** /**
@@ -169,7 +178,7 @@ public class Engine implements AutoCloseable, Cloneable {
* @param input inline JSON input. * @param input inline JSON input.
*/ */
public void setInputJson(String input) { public void setInputJson(String input) {
nativeSetInputJson(enginePtr, input); nativeSetInputJson(requireOpen(), input);
} }
/** /**
@@ -178,7 +187,7 @@ public class Engine implements AutoCloseable, Cloneable {
* @param path Path to JSON input. * @param path Path to JSON input.
*/ */
public void setInputJsonFromFile(String path) { public void setInputJsonFromFile(String path) {
nativeSetInputJsonFromFile(enginePtr, path); nativeSetInputJsonFromFile(requireOpen(), path);
} }
/** /**
@@ -189,7 +198,7 @@ public class Engine implements AutoCloseable, Cloneable {
* @return Query results as a JSON string. * @return Query results as a JSON string.
*/ */
public String evalQuery(String query) { public String evalQuery(String query) {
return nativeEvalQuery(enginePtr, query); return nativeEvalQuery(requireOpen(), query);
} }
/** /**
@@ -200,7 +209,7 @@ public class Engine implements AutoCloseable, Cloneable {
* @return Value of the rule as a JSON string. * @return Value of the rule as a JSON string.
*/ */
public String evalRule(String rule) { public String evalRule(String rule) {
return nativeEvalRule(enginePtr, rule); return nativeEvalRule(requireOpen(), rule);
} }
/** /**
@@ -210,7 +219,7 @@ public class Engine implements AutoCloseable, Cloneable {
* *
*/ */
public void setEnableCoverage(boolean enable) { public void setEnableCoverage(boolean enable) {
nativeSetEnableCoverage(enginePtr, enable); nativeSetEnableCoverage(requireOpen(), enable);
} }
/** /**
@@ -218,7 +227,7 @@ public class Engine implements AutoCloseable, Cloneable {
* *
*/ */
public void clearCoverageData() { public void clearCoverageData() {
nativeClearCoverageData(enginePtr); nativeClearCoverageData(requireOpen());
} }
/** /**
@@ -228,7 +237,7 @@ public class Engine implements AutoCloseable, Cloneable {
* *
*/ */
public String getCoverageReport() { public String getCoverageReport() {
return nativeGetCoverageReport(enginePtr); return nativeGetCoverageReport(requireOpen());
} }
/** /**
@@ -238,7 +247,7 @@ public class Engine implements AutoCloseable, Cloneable {
* *
*/ */
public String getCoverageReportPretty() { public String getCoverageReportPretty() {
return nativeGetCoverageReportPretty(enginePtr); return nativeGetCoverageReportPretty(requireOpen());
} }
/** /**
@@ -248,7 +257,7 @@ public class Engine implements AutoCloseable, Cloneable {
* *
*/ */
public void setGatherPrints(boolean b) { public void setGatherPrints(boolean b) {
nativeSetGatherPrints(enginePtr, b); nativeSetGatherPrints(requireOpen(), b);
} }
/** /**
@@ -258,7 +267,7 @@ public class Engine implements AutoCloseable, Cloneable {
* *
*/ */
public String takePrints() { public String takePrints() {
return nativeTakePrints(enginePtr); return nativeTakePrints(requireOpen());
} }
/** /**
@@ -267,24 +276,34 @@ public class Engine implements AutoCloseable, Cloneable {
* @param config Policy length configuration. * @param config Policy length configuration.
*/ */
public void setPolicyLengthConfig(PolicyLengthConfig config) { 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. * Clear the policy length configuration, reverting to defaults.
*/ */
public void clearPolicyLengthConfig() { public void clearPolicyLengthConfig() {
nativeClearPolicyLengthConfig(enginePtr); nativeClearPolicyLengthConfig(requireOpen());
} }
long getPtr() { long getPtr() {
return requireOpen();
}
private long requireOpen() {
if (enginePtr == 0) {
throw new IllegalStateException("Engine is closed");
}
return enginePtr; return enginePtr;
} }
@Override @Override
public void close() { public void close() {
nativeDestroyEngine(enginePtr); if (enginePtr != 0) {
nativeDestroyEngine(enginePtr);
enginePtr = 0;
}
} }
// Loading native library from JAR is adapted from: // Loading native library from JAR is adapted from:

View File

@@ -22,8 +22,19 @@ public class EngineTest extends TestCase
"package test\nmessage = concat(\", \", [input.message, data.message])" "package test\nmessage = concat(\", \", [input.message, data.message])"
); );
engine.addDataJson("{\"message\":\"World!\"}"); engine.addDataJson("{\"message\":\"World!\"}");
engine.prepare();
engine.setInputJson("{\"message\":\"Hello\"}"); engine.setInputJson("{\"message\":\"Hello\"}");
resJson = engine.evalQuery("data.test.message"); 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(); Gson gson = new Gson();
@@ -33,4 +44,28 @@ public class EngineTest extends TestCase
Map expression = (Map) expressions.get(0); Map expression = (Map) expressions.get(0);
Assert.assertEquals("Hello, World!", expression.get("value")); 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) {
}
}
} }

View File

@@ -463,6 +463,13 @@ impl Engine {
self.engine.take_prints() 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`] /// Clone a [`Engine`]
/// ///
/// To avoid having to parse same policy again, the engine can be cloned /// To avoid having to parse same policy again, the engine can be cloned

View File

@@ -87,6 +87,7 @@ report = engine.get_coverage_report_pretty()
print(report) print(report)
# Clone engine # Clone engine
engine.prepare()
engine1 = engine.clone() engine1 = engine.clone()

View File

@@ -115,6 +115,13 @@ impl Engine {
Ok(()) 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> { fn get_packages(&self) -> Result<Vec<String>, Error> {
self.engine self.engine
.borrow() .borrow()
@@ -373,6 +380,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
method!(Engine::add_data_from_json_file, 1), method!(Engine::add_data_from_json_file, 1),
)?; )?;
engine_class.define_method("clear_data", method!(Engine::clear_data, 0))?; engine_class.define_method("clear_data", method!(Engine::clear_data, 0))?;
engine_class.define_method("prepare", method!(Engine::prepare, 0))?;
// input operations // input operations
engine_class.define_method("set_input", method!(Engine::set_input, 1))?; engine_class.define_method("set_input", method!(Engine::set_input, 1))?;

View File

@@ -150,6 +150,7 @@ class TestRegorus < Minitest::Test
end end
def test_engine_cloning def test_engine_cloning
@engine.prepare
cloned_engine = @engine.clone cloned_engine = @engine.clone
assert_instance_of ::Regorus::Engine, cloned_engine assert_instance_of ::Regorus::Engine, cloned_engine

View File

@@ -21,3 +21,9 @@ Run `cargo xtask build-wasm` to invoke wasm-pack with sensible defaults, or `car
## Usage ## Usage
See [test.js](https://github.com/microsoft/regorus/blob/main/bindings/wasm/test.js) for example 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.

View File

@@ -138,6 +138,17 @@ impl Engine {
self.engine.set_rego_v0(enable) 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 /// Add a policy
/// ///
/// The policy is parsed into AST. /// The policy is parsed into AST.
@@ -158,6 +169,20 @@ impl Engine {
self.engine.add_data(data).map_err(error_to_jsvalue) 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. /// Get the list of packages defined by loaded policies.
/// ///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.get_packages /// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.get_packages
@@ -487,6 +512,9 @@ mod tests {
)?; )?;
assert_eq!(pkg, "data.test"); assert_eq!(pkg, "data.test");
// Prepare before first evaluation.
engine.prepare()?;
let results = engine.evalQuery("data".to_string())?; let results = engine.evalQuery("data".to_string())?;
let r = regorus::Value::from_json_str(&results).map_err(error_to_jsvalue)?; let r = regorus::Value::from_json_str(&results).map_err(error_to_jsvalue)?;

View File

@@ -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 // Set policy input
engine.setInputJson(` engine.setInputJson(`
{ {

View File

@@ -505,6 +505,47 @@ impl Engine {
self.add_data(Value::from_json_str(data_json)?) 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. /// Set whether builtins should raise errors strictly or not.
/// ///
/// Regorus differs from OPA in that by default builtins will /// 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))?; limits::enforce_memory_limit().map_err(|err| anyhow!(err))?;
self.interpreter.set_traces(enable_tracing); 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 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. // Analyze the modules and determine how statements must be scheduled.
let analyzer = Analyzer::new(); let analyzer = Analyzer::new();
let schedule = Rc::new(analyzer.analyze(&self.modules)?); let schedule = Rc::new(analyzer.analyze(&self.modules)?);
@@ -1116,23 +1158,28 @@ impl Engine {
// Set schedule after hoisting completes // Set schedule after hoisting completes
self.interpreter.set_schedule(Some(schedule)); self.interpreter.set_schedule(Some(schedule));
}
#[cfg(feature = "azure_policy")] #[cfg(feature = "azure_policy")]
{
if for_target { 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( crate::interpreter::target::resolve::resolve_and_apply_target(
&mut self.interpreter, &mut self.interpreter,
)?; )?;
// Infer resource types // Infer resource types
crate::interpreter::target::infer::infer_resource_type(&mut self.interpreter)?; crate::interpreter::target::infer::infer_resource_type(&mut self.interpreter)?;
} } else if newly_prepared {
// Check if any module specifies a target and warn if so.
if !for_target {
// Check if any module specifies a target and warn if so
#[cfg(feature = "azure_policy")]
self.warn_if_targets_present(); self.warn_if_targets_present();
} }
}
#[cfg(not(feature = "azure_policy"))]
let _ = for_target;
if newly_prepared {
self.prepared = true; self.prepared = true;
} }

View File

@@ -102,6 +102,135 @@ fn extension_with_state() -> Result<()> {
Ok(()) 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] #[test]
#[cfg(feature = "azure_policy")] #[cfg(feature = "azure_policy")]
#[cfg_attr(docsrs, doc(cfg(feature = "azure_policy")))] #[cfg_attr(docsrs, doc(cfg(feature = "azure_policy")))]