Files
regorus/src/engine.rs
Anand Krishnamoorthi c963e477a3 feat: Update to OPA v1.2.0 (#373)
Regorus now defaults to rego v1. `import rego.v1` is no longer needed.
Additionally, `future` keywords are automatically imported.

See
https://www.openpolicyagent.org/docs/latest/v0-upgrade/#changes-to-rego-in-opa-v10
to understand the differences between rego v1 and v0.

BREAKING CHANGE:

v0 style policies will error out by default. To enable v0 behavior, call engine.set_rego_v0(true) before
loading policies.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2025-03-10 11:56:01 -07:00

918 lines
31 KiB
Rust

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use crate::ast::*;
use crate::interpreter::*;
use crate::lexer::*;
use crate::parser::*;
use crate::scheduler::*;
use crate::utils::gather_functions;
use crate::value::*;
use crate::*;
use crate::{Extension, QueryResults};
use anyhow::{bail, Result};
/// The Rego evaluation engine.
///
#[derive(Debug, Clone)]
pub struct Engine {
modules: Vec<Ref<Module>>,
interpreter: Interpreter,
prepared: bool,
rego_v1: bool,
}
/// Create a default engine.
impl Default for Engine {
fn default() -> Self {
Self::new()
}
}
impl Engine {
/// Create an instance of [Engine].
pub fn new() -> Self {
Self {
modules: vec![],
interpreter: Interpreter::new(),
prepared: false,
rego_v1: true,
}
}
/// Enable rego v0.
///
/// Note that regorus now defaults to v1.
/// ```
/// # use regorus::*;
/// # fn main() -> anyhow::Result<()> {
/// let mut engine = Engine::new();
///
/// // Enable v0 for old style policies.
/// engine.set_rego_v0(true);
///
/// engine.add_policy(
/// "test.rego".to_string(),
/// r#"
/// package test
///
/// allow { # v0 syntax does not require if keyword
/// 1 < 2
/// }
/// "#.to_string())?;
///
/// # Ok(())
/// # }
/// ```
///
pub fn set_rego_v0(&mut self, rego_v0: bool) {
self.rego_v1 = !rego_v0;
}
/// Add a policy.
///
/// The policy file will be parsed and converted to AST representation.
/// Multiple policy files may be added to the engine.
/// Returns the Rego package name declared in the policy.
///
/// * `path`: A filename to be associated with the policy.
/// * `rego`: The rego policy code.
///
/// ```
/// # use regorus::*;
/// # fn main() -> anyhow::Result<()> {
/// let mut engine = Engine::new();
///
/// let package = engine.add_policy(
/// "test.rego".to_string(),
/// r#"
/// package test
/// allow = input.user == "root"
/// "#.to_string())?;
///
/// assert_eq!(package, "data.test");
/// # Ok(())
/// # }
/// ```
///
pub fn add_policy(&mut self, path: String, rego: String) -> Result<String> {
let source = Source::from_contents(path, rego)?;
let mut parser = self.make_parser(&source)?;
let module = Ref::new(parser.parse()?);
self.modules.push(module.clone());
// if policies change, interpreter needs to be prepared again
self.prepared = false;
Interpreter::get_path_string(&module.package.refr, Some("data"))
}
/// Add a policy from a given file.
///
/// The policy file will be parsed and converted to AST representation.
/// Multiple policy files may be added to the engine.
/// Returns the Rego package name declared in the policy.
///
/// * `path`: Path to the policy file (.rego).
///
/// ```
/// # use regorus::*;
/// # fn main() -> anyhow::Result<()> {
/// let mut engine = Engine::new();
/// // framework.rego does not conform to v1.
/// engine.set_rego_v0(true);
///
/// let package = engine.add_policy_from_file("tests/aci/framework.rego")?;
///
/// assert_eq!(package, "data.framework");
/// # Ok(())
/// # }
/// ```
#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
pub fn add_policy_from_file<P: AsRef<std::path::Path>>(&mut self, path: P) -> Result<String> {
let source = Source::from_file(path)?;
let mut parser = self.make_parser(&source)?;
let module = Ref::new(parser.parse()?);
self.modules.push(module.clone());
// if policies change, interpreter needs to be prepared again
self.prepared = false;
Interpreter::get_path_string(&module.package.refr, Some("data"))
}
/// Get the list of packages defined by loaded policies.
///
/// ```
/// # use regorus::*;
/// # fn main() -> anyhow::Result<()> {
/// let mut engine = Engine::new();
/// // framework.rego does not conform to v1.
/// engine.set_rego_v0(true);
///
/// let _ = engine.add_policy_from_file("tests/aci/framework.rego")?;
///
/// // Package names can be different from file names.
/// let _ = engine.add_policy("policy.rego".into(), "package hello.world".into())?;
///
/// assert_eq!(engine.get_packages()?, vec!["data.framework", "data.hello.world"]);
/// # Ok(())
/// # }
/// ```
pub fn get_packages(&self) -> Result<Vec<String>> {
self.modules
.iter()
.map(|m| Interpreter::get_path_string(&m.package.refr, Some("data")))
.collect()
}
/// Get the list of policy files.
/// ```
/// # use regorus::*;
/// # fn main() -> anyhow::Result<()> {
/// # let mut engine = Engine::new();
///
/// let pkg = engine.add_policy("hello.rego".to_string(), "package test".to_string())?;
/// assert_eq!(pkg, "data.test");
///
/// let policies = engine.get_policies()?;
///
/// assert_eq!(policies[0].get_path(), "hello.rego");
/// assert_eq!(policies[0].get_contents(), "package test");
/// # Ok(())
/// # }
/// ```
pub fn get_policies(&self) -> Result<Vec<Source>> {
Ok(self
.modules
.iter()
.map(|m| m.package.refr.span().source.clone())
.collect())
}
/// Get the list of policy files as a JSON object.
/// ```
/// # use regorus::*;
/// # fn main() -> anyhow::Result<()> {
/// # let mut engine = Engine::new();
///
/// let pkg = engine.add_policy("hello.rego".to_string(), "package test".to_string())?;
/// assert_eq!(pkg, "data.test");
///
/// let policies = engine.get_policies_as_json()?;
///
/// let v = Value::from_json_str(&policies)?;
/// assert_eq!(v[0]["path"].as_string()?.as_ref(), "hello.rego");
/// assert_eq!(v[0]["contents"].as_string()?.as_ref(), "package test");
/// # Ok(())
/// # }
/// ```
pub fn get_policies_as_json(&self) -> Result<String> {
#[derive(Serialize)]
struct Source<'a> {
path: &'a String,
contents: &'a String,
}
let mut sources = vec![];
for m in self.modules.iter() {
let source = &m.package.refr.span().source;
sources.push(Source {
path: source.get_path(),
contents: source.get_contents(),
});
}
serde_json::to_string_pretty(&sources).map_err(anyhow::Error::msg)
}
/// Set the input document.
///
/// * `input`: Input documented. Typically this [Value] is constructed from JSON or YAML.
///
/// ```
/// # use regorus::*;
/// # fn main() -> anyhow::Result<()> {
/// let mut engine = Engine::new();
///
/// let input = Value::from_json_str(r#"
/// {
/// "role" : "admin",
/// "action": "delete"
/// }"#)?;
///
/// engine.set_input(input);
/// # Ok(())
/// # }
/// ```
pub fn set_input(&mut self, input: Value) {
self.interpreter.set_input(input);
}
pub fn set_input_json(&mut self, input_json: &str) -> Result<()> {
self.set_input(Value::from_json_str(input_json)?);
Ok(())
}
/// Clear the data document.
///
/// The data document will be reset to an empty object.
///
/// ```
/// # use regorus::*;
/// # fn main() -> anyhow::Result<()> {
/// let mut engine = Engine::new();
///
/// engine.clear_data();
///
/// // Evaluate data.
/// let results = engine.eval_query("data".to_string(), false)?;
///
/// // Assert that it is empty object.
/// assert_eq!(results.result.len(), 1);
/// assert_eq!(results.result[0].expressions.len(), 1);
/// assert_eq!(results.result[0].expressions[0].value, Value::new_object());
/// # Ok(())
/// # }
/// ```
pub fn clear_data(&mut self) {
self.interpreter.set_init_data(Value::new_object());
self.prepared = false;
}
/// Add data document.
///
/// The specified data document is merged into existing data document.
///
/// ```
/// # use regorus::*;
/// # fn main() -> anyhow::Result<()> {
/// let mut engine = Engine::new();
///
/// // Only objects can be added.
/// assert!(engine.add_data(Value::from_json_str("[]")?).is_err());
///
/// // Merge { "x" : 1, "y" : {} }
/// assert!(engine.add_data(Value::from_json_str(r#"{ "x" : 1, "y" : {}}"#)?).is_ok());
///
/// // Merge { "z" : 2 }
/// assert!(engine.add_data(Value::from_json_str(r#"{ "z" : 2 }"#)?).is_ok());
///
/// // Merge { "z" : 3 }. Conflict error.
/// assert!(engine.add_data(Value::from_json_str(r#"{ "z" : 3 }"#)?).is_err());
///
/// assert_eq!(
/// engine.eval_query("data".to_string(), false)?.result[0].expressions[0].value,
/// Value::from_json_str(r#"{ "x": 1, "y": {}, "z": 2}"#)?
/// );
/// # Ok(())
/// # }
/// ```
pub fn add_data(&mut self, data: Value) -> Result<()> {
if data.as_object().is_err() {
bail!("data must be object");
}
self.prepared = false;
self.interpreter.get_init_data_mut().merge(data)
}
/// Get the data document.
///
/// The returned value is the data document that has been constructed using
/// one or more calls to [`Engine::add_data`]. The values of policy rules are
/// not included in the returned document.
///
///
/// ```
/// # use regorus::*;
/// # fn main() -> anyhow::Result<()> {
/// let mut engine = Engine::new();
///
/// // If not set, data document is empty.
/// assert_eq!(engine.get_data(), Value::new_object());
///
/// // Merge { "x" : 1, "y" : {} }
/// assert!(engine.add_data(Value::from_json_str(r#"{ "x" : 1, "y" : {}}"#)?).is_ok());
///
/// // Merge { "z" : 2 }
/// assert!(engine.add_data(Value::from_json_str(r#"{ "z" : 2 }"#)?).is_ok());
///
/// let data = engine.get_data();
/// assert_eq!(data["x"], Value::from(1));
/// assert_eq!(data["y"], Value::new_object());
/// assert_eq!(data["z"], Value::from(2));
///
/// # Ok(())
/// # }
/// ```
pub fn get_data(&self) -> Value {
self.interpreter.get_init_data().clone()
}
pub fn add_data_json(&mut self, data_json: &str) -> Result<()> {
self.add_data(Value::from_json_str(data_json)?)
}
/// Set whether builtins should raise errors strictly or not.
///
/// Regorus differs from OPA in that by default builtins will
/// raise errors instead of returning Undefined.
///
/// ----
/// **_NOTE:_** Currently not all builtins honor this flag and will always strictly raise errors.
/// ----
pub fn set_strict_builtin_errors(&mut self, b: bool) {
self.interpreter.set_strict_builtin_errors(b)
}
#[doc(hidden)]
pub fn get_modules(&mut self) -> &Vec<Ref<Module>> {
&self.modules
}
/// Evaluate specified rule(s).
///
/// [`Engine::eval_rule`] is often faster than [`Engine::eval_query`] and should be preferred if
/// OPA style [`QueryResults`] are not needed.
///
/// ```
/// # use regorus::*;
/// # fn main() -> anyhow::Result<()> {
/// let mut engine = Engine::new();
///
/// // Add policy
/// engine.add_policy(
/// "policy.rego".to_string(),
/// r#"
/// package example
/// import rego.v1
///
/// x = [1, 2]
///
/// y := 5 if input.a > 2
/// "#.to_string())?;
///
/// // Evaluate rule.
/// let v = engine.eval_rule("data.example.x".to_string())?;
/// assert_eq!(v, Value::from(vec![Value::from(1), Value::from(2)]));
///
/// // y evaluates to undefined.
/// let v = engine.eval_rule("data.example.y".to_string())?;
/// assert_eq!(v, Value::Undefined);
///
/// // Evaluating a non-existent rule is an error.
/// let r = engine.eval_rule("data.exaample.x".to_string());
/// assert!(r.is_err());
///
/// // Path must be valid rule paths.
/// assert!( engine.eval_rule("data".to_string()).is_err());
/// assert!( engine.eval_rule("data.example".to_string()).is_err());
/// # Ok(())
/// # }
/// ```
pub fn eval_rule(&mut self, rule: String) -> Result<Value> {
self.prepare_for_eval(false)?;
self.interpreter.clean_internal_evaluation_state();
self.interpreter.eval_rule_in_path(rule)
}
/// Evaluate a Rego query.
///
/// ```
/// # use regorus::*;
/// # fn main() -> anyhow::Result<()> {
/// let mut engine = Engine::new();
///
/// // Add policies
/// engine.set_rego_v0(true);
/// engine.add_policy_from_file("tests/aci/framework.rego")?;
/// engine.add_policy_from_file("tests/aci/api.rego")?;
/// engine.add_policy_from_file("tests/aci/policy.rego")?;
///
/// // Add data document (if any).
/// // If multiple data documents can be added, they will be merged together.
/// engine.add_data(Value::from_json_file("tests/aci/data.json")?)?;
///
/// // At this point the policies and data have been loaded.
/// // Either the same engine can be used to make multiple queries or the engine
/// // can be cloned to avoid having the reload the policies and data.
/// let _clone = engine.clone();
///
/// // Evaluate a query.
/// // Load input and make query.
/// engine.set_input(Value::new_object());
/// let results = engine.eval_query("data.framework.mount_overlay.allowed".to_string(), false)?;
/// assert_eq!(results.result[0].expressions[0].value, Value::from(false));
///
/// // Evaluate query with different inputs.
/// engine.set_input(Value::from_json_file("tests/aci/input.json")?);
/// let results = engine.eval_query("data.framework.mount_overlay.allowed".to_string(), false)?;
/// assert_eq!(results.result[0].expressions[0].value, Value::from(true));
/// # Ok(())
/// # }
/// ```
pub fn eval_query(&mut self, query: String, enable_tracing: bool) -> Result<QueryResults> {
self.prepare_for_eval(enable_tracing)?;
self.interpreter.clean_internal_evaluation_state();
self.interpreter.create_rule_prefixes()?;
let query_module = {
let source = Source::from_contents(
"<query_module.rego>".to_owned(),
"package __internal_query_module".to_owned(),
)?;
Ref::new(Parser::new(&source)?.parse()?)
};
// Parse the query.
let query_source = Source::from_contents("<query.rego>".to_string(), query)?;
let mut parser = self.make_parser(&query_source)?;
let query_node = parser.parse_user_query()?;
if query_node.span.text() == "data" {
self.eval_modules(enable_tracing)?;
}
let query_schedule = Analyzer::new().analyze_query_snippet(&self.modules, &query_node)?;
self.interpreter.eval_user_query(
&query_module,
&query_node,
&query_schedule,
enable_tracing,
)
}
/// Evaluate a Rego query that produces a boolean value.
///
///
/// This function should be preferred over [`Engine::eval_query`] if just a `true`/`false`
/// value is desired instead of [`QueryResults`].
///
/// ```
/// # use regorus::*;
/// # fn main() -> anyhow::Result<()> {
/// # let mut engine = Engine::new();
///
/// let enable_tracing = false;
/// assert_eq!(engine.eval_bool_query("1 > 2".to_string(), enable_tracing)?, false);
/// assert_eq!(engine.eval_bool_query("1 < 2".to_string(), enable_tracing)?, true);
///
/// // Non boolean queries will raise an error.
/// assert!(engine.eval_bool_query("1+1".to_string(), enable_tracing).is_err());
///
/// // Queries producing multiple values will raise an error.
/// assert!(engine.eval_bool_query("true; true".to_string(), enable_tracing).is_err());
///
/// // Queries producing no values will raise an error.
/// assert!(engine.eval_bool_query("true; false; true".to_string(), enable_tracing).is_err());
/// # Ok(())
/// # }
/// ```
pub fn eval_bool_query(&mut self, query: String, enable_tracing: bool) -> Result<bool> {
let results = self.eval_query(query, enable_tracing)?;
match results.result.len() {
0 => bail!("query did not produce any values"),
1 if results.result[0].expressions.len() == 1 => {
results.result[0].expressions[0].value.as_bool().copied()
}
_ => bail!("query produced more than one value"),
}
}
/// Evaluate an `allow` query.
///
/// This is a wrapper over [`Engine::eval_bool_query`] that returns true only if the
/// boolean query succeed and produced a `true` value.
///
/// ```
/// # use regorus::*;
/// # fn main() -> anyhow::Result<()> {
/// # let mut engine = Engine::new();
///
/// let enable_tracing = false;
/// assert_eq!(engine.eval_allow_query("1 > 2".to_string(), enable_tracing), false);
/// assert_eq!(engine.eval_allow_query("1 < 2".to_string(), enable_tracing), true);
///
/// assert_eq!(engine.eval_allow_query("1+1".to_string(), enable_tracing), false);
/// assert_eq!(engine.eval_allow_query("true; true".to_string(), enable_tracing), false);
/// assert_eq!(engine.eval_allow_query("true; false; true".to_string(), enable_tracing), false);
/// # Ok(())
/// # }
pub fn eval_allow_query(&mut self, query: String, enable_tracing: bool) -> bool {
matches!(self.eval_bool_query(query, enable_tracing), Ok(true))
}
/// Evaluate a `deny` query.
///
/// This is a wrapper over [`Engine::eval_bool_query`] that returns false only if the
/// boolean query succeed and produced a `false` value.
/// ```
/// # use regorus::*;
/// # fn main() -> anyhow::Result<()> {
/// # let mut engine = Engine::new();
///
/// let enable_tracing = false;
/// assert_eq!(engine.eval_deny_query("1 > 2".to_string(), enable_tracing), false);
/// assert_eq!(engine.eval_deny_query("1 < 2".to_string(), enable_tracing), true);
///
/// assert_eq!(engine.eval_deny_query("1+1".to_string(), enable_tracing), true);
/// assert_eq!(engine.eval_deny_query("true; true".to_string(), enable_tracing), true);
/// assert_eq!(engine.eval_deny_query("true; false; true".to_string(), enable_tracing), true);
/// # Ok(())
/// # }
pub fn eval_deny_query(&mut self, query: String, enable_tracing: bool) -> bool {
!matches!(self.eval_bool_query(query, enable_tracing), Ok(false))
}
#[doc(hidden)]
/// Evaluate the given query and all the rules in the supplied policies.
///
/// This is mainly used for testing Regorus itself.
pub fn eval_query_and_all_rules(
&mut self,
query: String,
enable_tracing: bool,
) -> Result<QueryResults> {
self.eval_modules(enable_tracing)?;
let query_module = {
let source = Source::from_contents(
"<query_module.rego>".to_owned(),
"package __internal_query_module".to_owned(),
)?;
Ref::new(Parser::new(&source)?.parse()?)
};
// Parse the query.
let query_source = Source::from_contents("<query.rego>".to_string(), query)?;
let mut parser = self.make_parser(&query_source)?;
let query_node = parser.parse_user_query()?;
let query_schedule = Analyzer::new().analyze_query_snippet(&self.modules, &query_node)?;
self.interpreter.eval_user_query(
&query_module,
&query_node,
&query_schedule,
enable_tracing,
)
}
#[doc(hidden)]
fn prepare_for_eval(&mut self, enable_tracing: bool) -> Result<()> {
self.interpreter.set_traces(enable_tracing);
// if the data/policies have changed or the interpreter has never been prepared
if !self.prepared {
// Analyze the modules and determine how statements must be scheduled.
let analyzer = Analyzer::new();
let schedule = analyzer.analyze(&self.modules)?;
self.interpreter.set_schedule(Some(schedule));
self.interpreter.set_modules(&self.modules);
self.interpreter.clear_builtins_cache();
// clean_internal_evaluation_state will set data to an efficient clont of use supplied init_data
// Initialize the with-document with initial data values.
// with-modifiers will be applied to this document.
self.interpreter.init_with_document()?;
self.interpreter
.set_functions(gather_functions(&self.modules)?);
self.interpreter.gather_rules()?;
self.interpreter.process_imports()?;
self.prepared = true;
}
Ok(())
}
#[doc(hidden)]
pub fn eval_rule_in_module(
&mut self,
module: &Ref<Module>,
rule: &Ref<Rule>,
enable_tracing: bool,
) -> Result<Value> {
self.prepare_for_eval(enable_tracing)?;
self.interpreter.clean_internal_evaluation_state();
self.interpreter.eval_rule(module, rule)?;
Ok(self.interpreter.get_data_mut().clone())
}
#[doc(hidden)]
pub fn eval_modules(&mut self, enable_tracing: bool) -> Result<Value> {
self.prepare_for_eval(enable_tracing)?;
self.interpreter.clean_internal_evaluation_state();
// Ensure that empty modules are created.
for m in self.modules.iter().filter(|m| m.policy.is_empty()) {
let path = Parser::get_path_ref_components(&m.package.refr)?;
let path: Vec<&str> = path.iter().map(|s| s.text()).collect();
let vref =
Interpreter::make_or_get_value_mut(self.interpreter.get_data_mut(), &path[..])?;
if *vref == Value::Undefined {
*vref = Value::new_object();
}
}
self.interpreter.check_default_rules()?;
for module in self.modules.clone() {
for rule in &module.policy {
self.interpreter.eval_rule(&module, rule)?;
}
}
// Defer the evaluation of the default rules to here
for module in self.modules.clone() {
let prev_module = self.interpreter.set_current_module(Some(module.clone()))?;
for rule in &module.policy {
self.interpreter.eval_default_rule(rule)?;
}
self.interpreter.set_current_module(prev_module)?;
}
// Ensure that all modules are created.
for m in &self.modules {
let path = Parser::get_path_ref_components(&m.package.refr)?;
let path: Vec<&str> = path.iter().map(|s| s.text()).collect();
let vref =
Interpreter::make_or_get_value_mut(self.interpreter.get_data_mut(), &path[..])?;
if *vref == Value::Undefined {
*vref = Value::new_object();
}
}
self.interpreter.create_rule_prefixes()?;
Ok(self.interpreter.get_data_mut().clone())
}
/// Add a custom builtin (extension).
///
/// * `path`: The fully qualified path of the builtin.
/// * `nargs`: The number of arguments the builtin takes.
/// * `extension`: The [`Extension`] instance.
///
/// ```rust
/// # use regorus::*;
/// # use anyhow::{bail, Result};
/// # fn main() -> Result<()> {
/// let mut engine = Engine::new();
///
/// // Policy uses `do_magic` custom builtin.
/// engine.add_policy(
/// "test.rego".to_string(),
/// r#"package test
/// x = do_magic(1)
/// "#.to_string(),
/// )?;
///
/// // Evaluating fails since `do_magic` is not defined.
/// assert!(engine.eval_query("data.test.x".to_string(), false).is_err());
///
/// // Add extension to implement `do_magic`. The extension can be stateful.
/// let mut magic = 8;
/// engine.add_extension("do_magic".to_string(), 1 , Box::new(move | mut params: Vec<Value> | {
/// // params is mut and therefore individual values can be removed from it and modified.
/// // The number of parameters (1) has already been validated.
///
/// match &params[0].as_i64() {
/// Ok(i) => {
/// // Compute value
/// let v = *i + magic;
/// // Update extension state.
/// magic += 1;
/// Ok(Value::from(v))
/// }
/// // Extensions can raise errors. Regorus will add location information to
/// // the error.
/// _ => bail!("do_magic expects i64 value")
/// }
/// }))?;
///
/// // Evaluation will now succeed.
/// let r = engine.eval_query("data.test.x".to_string(), false)?;
/// assert_eq!(r.result[0].expressions[0].value.as_i64()?, 9);
///
/// // Cloning the engine will also clone the extension.
/// let mut engine1 = engine.clone();
///
/// // Evaluating again will return a different value since the extension is stateful.
/// let r = engine.eval_query("data.test.x".to_string(), false)?;
/// assert_eq!(r.result[0].expressions[0].value.as_i64()?, 10);
///
/// // The second engine has a clone of the extension.
/// let r = engine1.eval_query("data.test.x".to_string(), false)?;
/// assert_eq!(r.result[0].expressions[0].value.as_i64()?, 10);
///
/// // Once added, the extension cannot be replaced or removed.
/// assert!(engine.add_extension("do_magic".to_string(), 1, Box::new(|_:Vec<Value>| {
/// Ok(Value::Undefined)
/// })).is_err());
///
/// // Extensions don't support out-parameter syntax.
/// engine.add_policy(
/// "policy.rego".to_string(),
/// r#"package invalid
/// x = y if {
/// # y = do_magic(2)
/// do_magic(2, y) # y is supplied as an out parameter.
/// }
/// "#.to_string()
/// )?;
///
/// // Evaluation fails since rule x calls an extension with out parameter.
/// assert!(engine.eval_query("data.invalid.x".to_string(), false).is_err());
/// # Ok(())
/// # }
/// ```
pub fn add_extension(
&mut self,
path: String,
nargs: u8,
extension: Box<dyn Extension>,
) -> Result<()> {
self.interpreter.add_extension(path, nargs, extension)
}
#[cfg(feature = "coverage")]
#[cfg_attr(docsrs, doc(cfg(feature = "coverage")))]
/// Get the coverage report.
///
/// ```rust
/// # use regorus::*;
/// # use anyhow::{bail, Result};
/// # fn main() -> Result<()> {
/// let mut engine = Engine::new();
///
/// engine.add_policy(
/// "policy.rego".to_string(),
/// r#"
/// package test # Line 2
///
/// x = y if { # Line 4
/// input.a > 2 # Line 5
/// y = 5 # Line 6
/// }
/// "#.to_string()
/// )?;
///
/// // Enable coverage.
/// engine.set_enable_coverage(true);
///
/// engine.eval_query("data".to_string(), false)?;
///
/// let report = engine.get_coverage_report()?;
/// assert_eq!(report.files[0].path, "policy.rego");
///
/// // Only line 5 is evaluated.
/// assert_eq!(report.files[0].covered.iter().cloned().collect::<Vec<u32>>(), vec![5]);
///
/// // Line 4 and 6 are not evaluated.
/// assert_eq!(report.files[0].not_covered.iter().cloned().collect::<Vec<u32>>(), vec![4, 6]);
/// # Ok(())
/// # }
/// ```
///
/// See also [`crate::coverage::Report::to_colored_string`].
pub fn get_coverage_report(&self) -> Result<crate::coverage::Report> {
self.interpreter.get_coverage_report()
}
#[cfg(feature = "coverage")]
#[cfg_attr(docsrs, doc(cfg(feature = "coverage")))]
/// Enable/disable policy coverage.
///
/// If `enable` is different from the current value, then any existing coverage
/// information will be cleared.
pub fn set_enable_coverage(&mut self, enable: bool) {
self.interpreter.set_enable_coverage(enable)
}
#[cfg(feature = "coverage")]
#[cfg_attr(docsrs, doc(cfg(feature = "coverage")))]
/// Clear the gathered policy coverage data.
pub fn clear_coverage_data(&mut self) {
self.interpreter.clear_coverage_data()
}
/// Gather output from print statements instead of emiting to stderr.
///
/// See [`Engine::take_prints`].
pub fn set_gather_prints(&mut self, b: bool) {
self.interpreter.set_gather_prints(b);
}
/// Take the gathered output of print statements.
///
/// ```rust
/// # use regorus::*;
/// # use anyhow::{bail, Result};
/// # fn main() -> Result<()> {
/// let mut engine = Engine::new();
///
/// // Print to stderr.
/// engine.eval_query("print(\"Hello\")".to_string(), false)?;
///
/// // Configure gathering print statements.
/// engine.set_gather_prints(true);
///
/// // Execute query.
/// engine.eval_query("print(\"Hello\")".to_string(), false)?;
///
/// // Take and clear prints.
/// let prints = engine.take_prints()?;
/// assert_eq!(prints.len(), 1);
/// assert!(prints[0].contains("Hello"));
///
/// for p in prints {
/// println!("{p}");
/// }
/// # Ok(())
/// # }
/// ```
pub fn take_prints(&mut self) -> Result<Vec<String>> {
self.interpreter.take_prints()
}
/// Get the policies and corresponding AST.
///
///
/// ```rust
/// # use regorus::*;
/// # use anyhow::{bail, Result};
/// # fn main() -> Result<()> {
/// # let mut engine = Engine::new();
/// engine.add_policy("test.rego".to_string(), "package test\n x := 1".to_string())?;
///
/// let ast = engine.get_ast_as_json()?;
/// let value = Value::from_json_str(&ast)?;
///
/// assert_eq!(value[0]["ast"]["package"]["refr"]["Var"][1].as_string()?.as_ref(), "test");
/// # Ok(())
/// # }
/// ```
#[cfg(feature = "ast")]
#[cfg_attr(docsrs, doc(cfg(feature = "ast")))]
pub fn get_ast_as_json(&self) -> Result<String> {
#[derive(Serialize)]
struct Policy<'a> {
source: &'a Source,
version: u32,
ast: &'a Module,
}
let mut ast = vec![];
for m in &self.modules {
ast.push(Policy {
source: &m.package.span.source,
version: 1,
ast: m,
});
}
serde_json::to_string_pretty(&ast).map_err(anyhow::Error::msg)
}
fn make_parser<'a>(&self, source: &'a Source) -> Result<Parser<'a>> {
let mut parser = Parser::new(source)?;
if self.rego_v1 {
parser.enable_rego_v1()?;
}
Ok(parser)
}
}