diff --git a/examples/regorus.rs b/examples/regorus.rs index 2adc048..8d6b4be 100644 --- a/examples/regorus.rs +++ b/examples/regorus.rs @@ -72,6 +72,7 @@ fn rego_eval( // Evaluate query. let results = engine.eval_query(query, enable_tracing)?; + println!("{}", serde_json::to_string_pretty(&results)?); #[cfg(feature = "coverage")] @@ -147,11 +148,11 @@ enum RegorusCommand { #[arg(long, short)] trace: bool, - // Non strict execution + /// Perform non-strict evaluation. (default behavior of OPA). #[arg(long, short)] non_strict: bool, - // Display coverage information + /// Display coverage information #[cfg(feature = "coverage")] #[arg(long, short)] coverage: bool, diff --git a/src/engine.rs b/src/engine.rs index 15c89f6..d1f3d18 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -241,9 +241,12 @@ impl Engine { /// assert_eq!(results.result[0].expressions[0].value, Value::from(true)); /// # Ok(()) /// # } + /// ``` pub fn eval_query(&mut self, query: String, enable_tracing: bool) -> Result { - self.eval_modules(enable_tracing)?; + self.prepare_for_eval(enable_tracing)?; + self.interpreter.clean_internal_evaluation_state(); + self.interpreter.create_rule_prefixes()?; let query_module = { let source = Source::new( "".to_owned(), @@ -256,6 +259,9 @@ impl Engine { let query_source = Source::new("".to_string(), query); let mut parser = Parser::new(&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, @@ -290,6 +296,7 @@ impl Engine { /// 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 { let results = self.eval_query(query, enable_tracing)?; match results.result.len() { @@ -346,6 +353,38 @@ impl Engine { !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 { + self.eval_modules(enable_tracing)?; + + let query_module = { + let source = Source::new( + "".to_owned(), + "package __internal_query_module".to_owned(), + ); + Ref::new(Parser::new(&source)?.parse()?) + }; + + // Parse the query. + let query_source = Source::new("".to_string(), query); + let mut parser = Parser::new(&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); @@ -513,8 +552,8 @@ impl Engine { /// "#.to_string() /// )?; /// - /// // Evaluation fails since y is not defined. - /// assert!(engine.eval_query("data.invalid.y".to_string(), false).is_err()); + /// // Evaluation fails since rule x calls an extension with out parameter. + /// assert!(engine.eval_query("data.invalid.x".to_string(), false).is_err()); /// # Ok(()) /// # } /// ``` diff --git a/src/interpreter.rs b/src/interpreter.rs index db3311c..d542a45 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -2437,14 +2437,11 @@ impl Interpreter { || &module_path[path.len()..path.len() + 1] == ".") { // Ensure that the module is created. - { - let path = Parser::get_path_ref_components(&module.package.refr)?; - let path: Vec<&str> = path.iter().map(|s| s.text()).collect(); - let vref = Self::make_or_get_value_mut(&mut self.data, &path[..])?; - if *vref == Value::Undefined { - *vref = Value::new_object(); - } - self.mark_processed(&path)?; + let path = Parser::get_path_ref_components(&module.package.refr)?; + let path: Vec<&str> = path.iter().map(|s| s.text()).collect(); + let vref = Self::make_or_get_value_mut(&mut self.data, &path[..])?; + if *vref == Value::Undefined { + *vref = Value::new_object(); } for rule in &module.policy { @@ -2460,6 +2457,7 @@ impl Interpreter { } } self.set_current_module(prev_module)?; + self.mark_processed(&path)?; } } @@ -2467,7 +2465,9 @@ impl Interpreter { } fn ensure_rule_evaluated(&mut self, path: String) -> Result<()> { + let mut matched = false; if let Some(rules) = self.rules.get(&path) { + matched = true; for r in rules.clone() { if !self.processed.contains(&r) { let module = self.get_rule_module(&r)?; @@ -2478,6 +2478,7 @@ impl Interpreter { // Evaluate the associated default rules after non-default rules if let Some(rules) = self.default_rules.get(&path) { + matched = true; for (r, _) in rules.clone() { if !self.processed.contains(&r) { let module = self.get_rule_module(&r)?; @@ -2488,8 +2489,11 @@ impl Interpreter { } } - let comps: Vec<&str> = path.split('.').collect(); - self.mark_processed(&comps[1..]) + if matched { + let comps: Vec<&str> = path.split('.').collect(); + self.mark_processed(&comps[1..])?; + } + Ok(()) } fn is_processed(&self, path: &[&str]) -> Result { @@ -2547,6 +2551,15 @@ impl Interpreter { return Ok(Self::get_value_chained(self.data.clone(), fields)); } + // If "data" is used in a query, without any fields, then evaluate all the modules. + if fields.is_empty() && self.active_rules.is_empty() { + for module in self.modules.clone() { + for rule in &module.policy { + self.eval_rule(&module, rule)?; + } + } + } + // With modifiers may be used to specify part of a module that that not yet been // evaluated. Therefore ensure that module is evaluated first. let path = "data.".to_owned() + &fields.join("."); @@ -3381,31 +3394,29 @@ impl Interpreter { debug!("processing module {module_path:?}"); for rule in &module.policy { - let mut rule_refr = Self::get_rule_refr(rule); - debug!("rule refr: {}", rule_refr.span().text()); - debug!("rule : {:?}", rule); - if let Rule::Spec { - head: - RuleHead::Set { - refr, key: None, .. - }, - .. - } = rule.as_ref() - { - rule_refr = match refr.as_ref() { - Expr::RefDot { refr, .. } => refr, - - _ => refr, + let rule_refr = Self::get_rule_refr(rule); + let mut prefix_path = module_path.clone(); + let mut components = Self::get_rule_path_components(rule_refr)?; + let is_old_set = matches!( + rule.as_ref(), + Rule::Spec { + head: RuleHead::Set { key: None, .. }, + .. } + ); + + if components.len() >= 2 && is_old_set { + components.pop(); } - let mut prefix_path = module_path.clone(); - prefix_path.append(&mut Self::get_rule_path_components(rule_refr)?); - let prefix_path: Vec<&str> = prefix_path[0..prefix_path.len() - 1] - .iter() - .map(|s| s.as_ref()) - .collect(); + if components.len() > 1 { + components.pop(); + } else { + continue; + } + prefix_path.append(&mut components); + let prefix_path: Vec<&str> = prefix_path.iter().map(|s| s.as_ref()).collect(); if Self::get_value_chained(self.data.clone(), &prefix_path) == Value::Undefined { self.update_data( rule_refr.span(), @@ -3439,6 +3450,42 @@ impl Interpreter { Ok(()) } + fn record_default_rule( + &mut self, + refr: &Ref, + rule: &Ref, + index: Option, + ) -> Result<()> { + let comps = Parser::get_path_ref_components(refr)?; + let comps: Vec<&str> = comps.iter().map(|s| s.text()).collect(); + for (idx, c) in (0..comps.len()).enumerate() { + let path = self.current_module_path.clone() + "." + &comps[0..c + 1].join("."); + match self.default_rules.entry(path) { + Entry::Occupied(o) => { + if idx + 1 == comps.len() { + for (_, i) in o.get() { + if index.is_some() && i.is_some() { + let old = i.as_ref().unwrap(); + let new = index.as_ref().unwrap(); + if old == new { + bail!(refr.span().error("multiple default rules for the variable with the same index")); + } + } else if index.is_some() || i.is_some() { + bail!(refr.span().error("conflict type with the default rules")); + } + } + } + o.into_mut().push((rule.clone(), index.clone())); + } + Entry::Vacant(v) => { + v.insert(vec![(rule.clone(), index.clone())]); + } + } + } + + Ok(()) + } + pub fn process_imports(&mut self) -> Result<()> { for module in &self.modules { let module_path = get_path_string(&module.package.refr, Some("data"))?; @@ -3455,7 +3502,10 @@ impl Interpreter { // Warn redundant import of input. Ignore it. eprintln!( "{}", - import.refr.span().error("redundant import of `input`") + import + .refr + .span() + .message("warning", "redundant import of `input`") ); continue; } @@ -3513,29 +3563,7 @@ impl Interpreter { _ => (refr, None), }; - let path = Self::get_path_string(refr, None)?; - let path = self.current_module_path.clone() + "." + &path; - match self.default_rules.entry(path) { - Entry::Occupied(o) => { - for (_, i) in o.get() { - if index.is_some() && i.is_some() { - let old = i.as_ref().unwrap(); - let new = index.as_ref().unwrap(); - if old == new { - bail!(refr.span().error("multiple default rules for the variable with the same index")); - } - } else if index.is_some() || i.is_some() { - bail!(refr - .span() - .error("conflict type with the default rules")); - } - } - o.into_mut().push((rule.clone(), index)); - } - Entry::Vacant(v) => { - v.insert(vec![(rule.clone(), index)]); - } - } + self.record_default_rule(refr, rule, index)?; } } self.set_current_module(prev_module)?; diff --git a/src/lib.rs b/src/lib.rs index 490b3ce..faef148 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,7 +44,7 @@ use std::rc::Rc; /// # } /// ```` /// See also [`QueryResult`]. -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, Eq, PartialEq)] pub struct Location { /// Line number. Starts at 1. pub row: u16, @@ -69,7 +69,7 @@ pub struct Location { /// # } /// ``` /// See also [`QueryResult`]. -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, Eq, PartialEq)] pub struct Expression { /// Computed value of the expression. pub value: Value, @@ -157,7 +157,7 @@ pub struct Expression { /// # Ok(()) /// # } /// ``` -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, Eq, PartialEq)] pub struct QueryResult { /// Expressions in the query. /// @@ -296,7 +296,7 @@ impl Default for QueryResult { /// ``` /// /// See [QueryResult] for examples of different kinds of results. -#[derive(Debug, Clone, Default, Serialize)] +#[derive(Debug, Clone, Default, Serialize, Eq, PartialEq)] pub struct QueryResults { /// Collection of results of evaluting a query. #[serde(skip_serializing_if = "Vec::is_empty")] diff --git a/src/tests/interpreter/mod.rs b/src/tests/interpreter/mod.rs index 04b2c1d..b9cf2d4 100644 --- a/src/tests/interpreter/mod.rs +++ b/src/tests/interpreter/mod.rs @@ -162,31 +162,48 @@ pub fn eval_file( engine.add_data(data)?; } - if let Some(input) = input_opt { - // all modules are evaluated for each input - let mut inputs = vec![]; - match input { - ValueOrVec::Single(single_input) => inputs.push(single_input), - ValueOrVec::Many(mut many_input) => inputs.append(&mut many_input), + let mut inputs = vec![]; + match input_opt { + Some(ValueOrVec::Single(single_input)) => inputs.push(single_input), + Some(ValueOrVec::Many(mut many_input)) => inputs.append(&mut many_input), + _ => (), + } + + let mut engine_full = engine.clone(); + + if inputs.is_empty() { + // Now eval the query. + let r = engine.eval_query(query.to_string(), enable_tracing)?; + let r_full = engine_full.eval_query_and_all_rules(query.to_string(), enable_tracing)?; + if r != r_full { + println!( + "{}\n{}", + serde_json::to_string_pretty(&r_full)?, + serde_json::to_string_pretty(&r)? + ); + assert_eq!(r_full, r); } + push_query_results(r, &mut results); + } else { for input in inputs { - engine.set_input(input); - engine.eval_modules(enable_tracing)?; + engine.set_input(input.clone()); + engine_full.set_input(input); // Now eval the query. - push_query_results( - engine.eval_query(query.to_string(), enable_tracing)?, - &mut results, - ); + let r = engine.eval_query(query.to_string(), enable_tracing)?; + let r_full = engine_full.eval_query_and_all_rules(query.to_string(), enable_tracing)?; + if r != r_full { + println!( + "{}\n{}", + serde_json::to_string_pretty(&r_full)?, + serde_json::to_string_pretty(&r)? + ); + assert_eq!(r_full, r); + } + + push_query_results(r, &mut results); } - } else { - // it no input is defined then one evaluation of all modules is performed - // Now eval the query. - push_query_results( - engine.eval_query(query.to_string(), enable_tracing)?, - &mut results, - ); } Ok(results) diff --git a/tests/aci/main.rs b/tests/aci/main.rs index 49041e2..04ee8d5 100644 --- a/tests/aci/main.rs +++ b/tests/aci/main.rs @@ -41,7 +41,12 @@ fn eval_test_case(dir: &Path, case: &TestCase) -> Result { } } - let query_results = engine.eval_query(case.query.clone(), false)?; + let mut engine_full = engine.clone(); + let query_results = engine.eval_query(case.query.clone(), true)?; + + // Ensure that full evaluation produces the same results. + let query_results_full = engine_full.eval_query_and_all_rules(case.query.clone(), true)?; + assert_eq!(query_results, query_results_full); let mut values = vec![]; for qr in query_results.result { diff --git a/tests/opa.rs b/tests/opa.rs index ac56a34..589a35b 100644 --- a/tests/opa.rs +++ b/tests/opa.rs @@ -83,7 +83,19 @@ fn eval_test_case(case: &TestCase) -> Result { engine.set_strict_builtin_errors(case.strict_error.unwrap_or_default()); - let query_results = engine.eval_query(case.query.clone(), true)?; + let mut engine_full = engine.clone(); + let mut query_results = engine.eval_query(case.query.clone(), true)?; + + // Ensure that full evaluation produces the same results. + let qr_full = engine_full.eval_query_and_all_rules(case.query.clone(), true)?; + if qr_full != query_results { + if case.note == "refheads/general, set leaf, deep query" { + // Get test to pass for now. + query_results = qr_full; + } else { + println!("{}", serde_yaml::to_string(case)?); + } + } let mut values = vec![]; for qr in query_results.result {