diff --git a/src/interpreter.rs b/src/interpreter.rs index 3f6a0f2..3a95617 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -20,8 +20,10 @@ pub struct Interpreter<'source> { module: Option<&'source Module<'source>>, schedule: Option<&'source Schedule<'source>>, current_module_path: String, + prepared: bool, input: Value, data: Value, + init_data: Value, scopes: Vec, // TODO: handle recursive calls where same expr could have different values. loop_var_values: BTreeMap<&'source Expr<'source>, Value>, @@ -58,8 +60,10 @@ impl<'source> Interpreter<'source> { module: None, schedule: None, current_module_path: String::default(), + prepared: false, input: Value::new_object(), data: Value::new_object(), + init_data: Value::new_object(), scopes: vec![Scope::new()], contexts: vec![], loop_var_values: BTreeMap::new(), @@ -74,6 +78,44 @@ impl<'source> Interpreter<'source> { }) } + pub fn get_modules(&mut self) -> &mut Vec<&'source Module<'source>> { + &mut self.modules + } + + pub fn set_data(&mut self, data: Value) { + self.data = data; + } + + pub fn get_data(&mut self) -> &mut Value { + &mut self.data + } + + fn clean_internal_evaluation_state(&mut self) { + self.data = self.init_data.clone(); + self.processed.clear(); + self.loop_var_values.clear(); + self.scopes = vec![Scope::new()]; + self.contexts = vec![]; + } + + fn checks_for_eval(&mut self, input: &Option, enable_tracing: bool) -> Result<()> { + if !self.prepared { + bail!("prepare_for_eval should be called before eval_modules"); + } + + self.traces = match enable_tracing { + true => Some(vec![]), + false => None, + }; + + if let Some(input) = input { + self.input = input.clone(); + info!("input: {:#?}", self.input); + } + + Ok(()) + } + fn current_module(&self) -> Result<&'source Module<'source>> { self.module .ok_or_else(|| anyhow!("internal error: current module not set")) @@ -1435,6 +1477,7 @@ impl<'source> Interpreter<'source> { let module_path = Self::get_path_string(&self.current_module()?.package.refr, Some("data"))?; let path = module_path + "." + name; + self.ensure_rule_evaluated(path)?; let mut path: Vec<&str> = @@ -1933,62 +1976,65 @@ impl<'source> Interpreter<'source> { head: rule_head, bodies: rule_body, } => { - if matches!(rule_head, RuleHead::Func { .. }) { - return Ok(()); - } - - let (ctx, mut path) = self.make_rule_context(rule_head)?; - let special_set = matches!((ctx.output_expr, &ctx.value), (None, Value::Set(_))); - let value = match self.eval_rule_bodies(ctx, span, rule_body)? { - Value::Set(_) if special_set => { - let entry = path[path.len() - 1].text(); - let mut s = BTreeSet::new(); - s.insert(Value::String(entry.to_owned())); - path = path[0..path.len() - 1].to_vec(); - Value::from_set(s) + if !matches!(rule_head, RuleHead::Func { .. }) { + let (ctx, mut path) = self.make_rule_context(rule_head)?; + let special_set = + matches!((ctx.output_expr, &ctx.value), (None, Value::Set(_))); + let value = match self.eval_rule_bodies(ctx, span, rule_body)? { + Value::Set(_) if special_set => { + let entry = path[path.len() - 1].text(); + let mut s = BTreeSet::new(); + s.insert(Value::String(entry.to_owned())); + path = path[0..path.len() - 1].to_vec(); + Value::from_set(s) + } + v => v, + }; + if value != Value::Undefined { + let paths: Vec<&str> = path.iter().map(|s| s.text()).collect(); + let vref = Self::make_or_get_value_mut(&mut self.data, &paths[..])?; + Self::merge_value(span, vref, value)?; } - v => v, - }; - if value != Value::Undefined { - let paths: Vec<&str> = path.iter().map(|s| s.text()).collect(); - let vref = Self::make_or_get_value_mut(&mut self.data, &paths[..])?; - Self::merge_value(span, vref, value)?; + self.processed.insert(rule); } } _ => bail!("internal error: unexpected"), } self.set_current_module(prev_module)?; - self.processed.insert(rule); match self.active_rules.pop() { Some(r) if r == rule => Ok(()), _ => bail!("internal error: current rule not active"), } } - pub fn eval( + pub fn eval_rule_with_input( &mut self, - data: &Option, + module: &'source Module<'source>, + rule: &'source Rule<'source>, input: &Option, enable_tracing: bool, - schedule: Option<&'source Schedule<'source>>, ) -> Result { - self.schedule = schedule; - self.traces = match enable_tracing { - true => Some(vec![]), - false => None, - }; + self.checks_for_eval(input, enable_tracing)?; + self.clean_internal_evaluation_state(); + self.eval_rule(module, rule)?; + + Ok(self.data.clone()) + } + + pub fn prepare_for_eval( + &mut self, + schedule: Option<&'source Schedule<'source>>, + data: &Option, + ) -> Result<()> { + self.schedule = schedule; self.builtins_cache.clear(); - if let Some(input) = input { - self.input = input.clone(); - - info!("input: {:#?}", self.input); - } if let Some(data) = data { self.data = data.clone(); } + // Ensure that each module has an empty object for m in &self.modules { let path = Parser::get_path_ref_components(&m.package.refr)?; @@ -2003,6 +2049,39 @@ impl<'source> Interpreter<'source> { self.update_function_table()?; self.gather_rules()?; + self.init_data = self.data.clone(); + self.prepared = true; + + Ok(()) + } + + pub fn eval_module( + &mut self, + module: &'source Module<'source>, + input: &Option, + enable_tracing: bool, + ) -> Result { + self.checks_for_eval(input, enable_tracing)?; + self.clean_internal_evaluation_state(); + + for rule in &module.policy { + self.eval_rule(module, rule)?; + } + + // Defer the evaluation of the default rules to here + let prev_module = self.set_current_module(Some(module))?; + for rule in &module.policy { + self.eval_default_rule(rule)?; + } + self.set_current_module(prev_module)?; + + Ok(self.data.clone()) + } + + pub fn eval_modules(&mut self, input: &Option, enable_tracing: bool) -> Result { + self.checks_for_eval(input, enable_tracing)?; + self.clean_internal_evaluation_state(); + for module in self.modules.clone() { for rule in &module.policy { self.eval_rule(module, rule)?; @@ -2021,6 +2100,17 @@ impl<'source> Interpreter<'source> { Ok(self.data.clone()) } + pub fn eval( + &mut self, + data: &Option, + input: &Option, + enable_tracing: bool, + schedule: Option<&'source Schedule<'source>>, + ) -> Result { + self.prepare_for_eval(schedule, data)?; + self.eval_modules(input, enable_tracing) + } + pub fn eval_query_snippet( &mut self, snippet: &'source Expr<'source>, diff --git a/tests/interpreter/cases/arithmetic/mod.rs b/tests/interpreter/cases/arithmetic/mod.rs index d9e1cb8..ac02625 100644 --- a/tests/interpreter/cases/arithmetic/mod.rs +++ b/tests/interpreter/cases/arithmetic/mod.rs @@ -29,14 +29,14 @@ fn basic() -> Result<()> { } "#; - let expected = Value::from_json_str( + let expected = vec![Value::from_json_str( r#" { "add" : true, "sub" : true, "mul" : true, "div" : true }"#, - )?; + )?]; assert_eq!( eval_file(&[rego.to_owned()], None, None, "data.test", false)?, diff --git a/tests/interpreter/cases/compr/mod.rs b/tests/interpreter/cases/compr/mod.rs index bc07c03..f7cadbc 100644 --- a/tests/interpreter/cases/compr/mod.rs +++ b/tests/interpreter/cases/compr/mod.rs @@ -32,7 +32,7 @@ fn basic_array() -> Result<()> { array_compr_7 = [ 1 | [1, 2, 3][_]; [1, 2][_] >= 2 ] "#; - let expected = Value::from_json_str( + let expected = vec![Value::from_json_str( r#" { "array": [1, 2, 3], "array_compr_0": [1], @@ -44,7 +44,7 @@ fn basic_array() -> Result<()> { "array_compr_6": [1, 1, 1, 1, 1, 1], "array_compr_7": [1, 1, 1] }"#, - )?; + )?]; assert_match( eval_file(&[rego.to_owned()], None, None, "data.test", false)?, @@ -81,7 +81,7 @@ fn basic_set() -> Result<()> { set_compr_7 = { a | a = [1, 2, 3][_]; [1, 2][_] >= 2 } "#; - let expected = Value::from_json_str( + let expected = vec![Value::from_json_str( r#" { "set": { "set!": [1, "string", [2, 3, 4], 567, false] @@ -113,7 +113,7 @@ fn basic_set() -> Result<()> { "set!": [1, 2, 3] } }"#, - )?; + )?]; assert_match( eval_file(&[rego.to_owned()], None, None, "data.test", false)?, diff --git a/tests/interpreter/cases/compr/object.yaml b/tests/interpreter/cases/compr/object.yaml index 7366030..fd38ccc 100644 --- a/tests/interpreter/cases/compr/object.yaml +++ b/tests/interpreter/cases/compr/object.yaml @@ -64,4 +64,3 @@ cases: x = { k:v | k = ["Hello", "world", 1][_]; v = [1, 2][_] } query: data.test error: "value for key `\"Hello\"` generated multiple times: `1` and `2`" - want_result: diff --git a/tests/interpreter/cases/in/mod.rs b/tests/interpreter/cases/in/mod.rs index 02dd26f..1ac45ae 100644 --- a/tests/interpreter/cases/in/mod.rs +++ b/tests/interpreter/cases/in/mod.rs @@ -99,7 +99,7 @@ fn basic() -> Result<()> { } "#; - let expected = Value::from_json_str( + let expected = vec![Value::from_json_str( r#" { "array": [1, 2, 3], "in_array_key_value": true, @@ -117,7 +117,7 @@ fn basic() -> Result<()> { "in_set_value": true, "some_decl_set_value": true }"#, - )?; + )?]; assert_match( eval_file(&[rego.to_owned()], None, None, "data.test", false)?, diff --git a/tests/interpreter/cases/input/mod.rs b/tests/interpreter/cases/input/mod.rs new file mode 100644 index 0000000..f1e6943 --- /dev/null +++ b/tests/interpreter/cases/input/mod.rs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![cfg(test)] + +use crate::interpreter::*; +use anyhow::Result; + +#[test] +fn basic() -> Result<()> { + let rego = r#" + package test + + x[a] { + a = y + } + + y[a] { + a = input.x + 5 + } +"#; + + let input = ValueOrVec::Many(vec![ + Value::from_json_str(r#"{"x": 1}"#)?, + Value::from_json_str(r#"{"x": 6}"#)?, + ]); + + let expected = vec![ + Value::from_json_str( + r#" { + "y": {"set!": [6]}, + "x": {"set!": [{"set!":[6]}]} +}"#, + )?, + Value::from_json_str( + r#" { + "y": {"set!": [11]}, + "x": {"set!": [{"set!":[11]}]} +}"#, + )?, + ]; + + assert_match( + eval_file_first_rule(&[rego.to_owned()], None, Some(input), "data.test", false)?, + expected, + ); + Ok(()) +} diff --git a/tests/interpreter/cases/input/multiple.yaml b/tests/interpreter/cases/input/multiple.yaml new file mode 100644 index 0000000..067cb47 --- /dev/null +++ b/tests/interpreter/cases/input/multiple.yaml @@ -0,0 +1,33 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +cases: + - note: input-multiple-1 + data: {} + input: + many!: + - { x: 1 } + - { x: 5 } + modules: + - | + package test + + x[a] { + a = y + } + + y[a] { + a = input.x + 5 + } + + query: data.test + want_result: + many!: + - y: + set!: [6] + x: + set!: [ set!: [6] ] + - y: + set!: [10] + x: + set!: [ set!: [10] ] diff --git a/tests/interpreter/cases/input/simple.yaml b/tests/interpreter/cases/input/simple.yaml new file mode 100644 index 0000000..469e81e --- /dev/null +++ b/tests/interpreter/cases/input/simple.yaml @@ -0,0 +1,27 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +cases: + - note: input-basic-1 + data: {} + input: { + x: 1 + } + modules: + - | + package test + + x[a] { + a = y + } + + y[a] { + a = input.x + 5 + } + + query: data.test + want_result: + y: + set!: [6] + x: + set!: [ set!: [6] ] diff --git a/tests/interpreter/cases/mod.rs b/tests/interpreter/cases/mod.rs index 67e9f63..77934b8 100644 --- a/tests/interpreter/cases/mod.rs +++ b/tests/interpreter/cases/mod.rs @@ -5,4 +5,5 @@ mod arithmetic; mod builtins; mod compr; mod r#in; +mod input; mod variables; diff --git a/tests/interpreter/cases/variables/mod.rs b/tests/interpreter/cases/variables/mod.rs index 6a00b88..592d9a3 100644 --- a/tests/interpreter/cases/variables/mod.rs +++ b/tests/interpreter/cases/variables/mod.rs @@ -31,7 +31,7 @@ fn basic() -> Result<()> { set = {1, 2, 3} "#; - let expected = Value::from_json_str( + let expected = vec![Value::from_json_str( r#" { "array": [1, 2, 3], "nested_array": [1, [2, 3, 4], 5, 6], @@ -44,7 +44,7 @@ fn basic() -> Result<()> { "local_0": 10, "local_1": "test_local" }"#, - )?; + )?]; assert_match( eval_file(&[rego.to_owned()], None, None, "data.test", false)?, diff --git a/tests/interpreter/mod.rs b/tests/interpreter/mod.rs index d5f34a9..8638c13 100644 --- a/tests/interpreter/mod.rs +++ b/tests/interpreter/mod.rs @@ -7,7 +7,7 @@ use std::env; use anyhow::{bail, Result}; use regorus::*; -use serde::{Deserialize, Serialize}; +use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer}; use test_generator::test_resources; //use walkdir::WalkDir; @@ -154,28 +154,52 @@ fn match_values(computed: &Value, expected: &Value) -> Result<()> { } } -pub fn assert_match(computed: Value, expected: Value) { - let expected = match process_value(&expected) { - Ok(e) => e, - _ => panic!("unable to process value :\n {expected:?}"), - }; - match match_values(&computed, &expected) { - Ok(()) => (), - Err(e) => panic!("{}", e), +pub fn assert_match(computed_results: Vec, expected_results: Vec) { + if computed_results.len() != expected_results.len() { + panic!( + "the number of computed results ({}) and expected results ({}) is not equal", + computed_results.len(), + expected_results.len() + ); + } + + for (n, expected_result) in expected_results.into_iter().enumerate() { + let expected = match process_value(&expected_result) { + Ok(e) => e, + _ => panic!("unable to process value :\n {expected_result:?}"), + }; + + if let Some(computed_result) = computed_results.get(n) { + match match_values(computed_result, &expected) { + Ok(()) => (), + Err(e) => panic!("{}", e), + } + } } } -pub fn eval_file( +pub fn eval_file_first_rule( regos: &[String], - data: Option, - input: Option, + data_opt: Option, + input_opt: Option, query: &str, enable_tracing: bool, -) -> Result { +) -> Result> { + let mut results = vec![]; let mut files = vec![]; let mut sources = vec![]; let mut modules = vec![]; let mut modules_ref = vec![]; + + // the query is parsed for later + let source = Source { + file: "", + contents: query, + lines: query.split('\n').collect(), + }; + let mut parser = Parser::new(&source)?; + let expr = parser.parse_membership_expr()?; + for (idx, _) in regos.iter().enumerate() { files.push(format!("rego_{idx}")); } @@ -201,11 +225,53 @@ pub fn eval_file( let analyzer = Analyzer::new(); let schedule = analyzer.analyze(&modules)?; - // First eval the modules. let mut interpreter = interpreter::Interpreter::new(modules_ref)?; - interpreter.eval(&data, &input, enable_tracing, Some(&schedule))?; + if let Some(input) = input_opt { + // if inputs are defined then first the evaluation if prepared + interpreter.prepare_for_eval(Some(&schedule), &data_opt)?; - // Now eval the query. + // then 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), + } + + for input in inputs { + if let Some(module) = &modules.get(0) { + if let Some(rule) = &module.policy.get(0) { + interpreter.eval_rule_with_input(module, rule, &Some(input), enable_tracing)?; + } + } + + // Now eval the query. + results.push(interpreter.eval_query_snippet(&expr, enable_tracing)?); + } + } else { + // it no input is defined then one evaluation of all modules is performed + interpreter.eval(&data_opt, &None, enable_tracing, Some(&schedule))?; + + // Now eval the query. + results.push(interpreter.eval_query_snippet(&expr, enable_tracing)?); + } + + Ok(results) +} + +pub fn eval_file( + regos: &[String], + data_opt: Option, + input_opt: Option, + query: &str, + enable_tracing: bool, +) -> Result> { + let mut results = vec![]; + let mut files = vec![]; + let mut sources = vec![]; + let mut modules = vec![]; + let mut modules_ref = vec![]; + + // the query is parsed for later let source = Source { file: "", contents: query, @@ -213,7 +279,59 @@ pub fn eval_file( }; let mut parser = Parser::new(&source)?; let expr = parser.parse_membership_expr()?; - interpreter.eval_query_snippet(&expr, enable_tracing) + + for (idx, _) in regos.iter().enumerate() { + files.push(format!("rego_{idx}")); + } + + for (idx, file) in files.iter().enumerate() { + let contents = regos[idx].as_str(); + sources.push(Source { + file, + contents, + lines: contents.split('\n').collect(), + }); + } + + for source in &sources { + let mut parser = Parser::new(source)?; + modules.push(parser.parse()?); + } + + for m in &modules { + modules_ref.push(m); + } + + let analyzer = Analyzer::new(); + let schedule = analyzer.analyze(&modules)?; + + let mut interpreter = interpreter::Interpreter::new(modules_ref)?; + if let Some(input) = input_opt { + // if inputs are defined then first the evaluation if prepared + interpreter.prepare_for_eval(Some(&schedule), &data_opt)?; + + // then 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), + } + + for input in inputs { + interpreter.eval_modules(&Some(input), enable_tracing)?; + + // Now eval the query. + results.push(interpreter.eval_query_snippet(&expr, enable_tracing)?); + } + } else { + // it no input is defined then one evaluation of all modules is performed + interpreter.eval(&data_opt, &None, enable_tracing, Some(&schedule))?; + + // Now eval the query. + results.push(interpreter.eval_query_snippet(&expr, enable_tracing)?); + } + + Ok(results) } #[test] @@ -256,20 +374,57 @@ fn one_file() -> Result<()> { } let mut interpreter = interpreter::Interpreter::new(modules_ref)?; - let results = interpreter.eval(&None, &input, true, Some(&schedule))?; + interpreter.prepare_for_eval(Some(&schedule), &None)?; + let results = interpreter.eval_modules(&input, true)?; println!("eval results:\n{}", serde_json::to_string_pretty(&results)?); Ok(()) } +#[derive(PartialEq, Debug)] +pub enum ValueOrVec { + Single(Value), + Many(Vec), +} + +impl Serialize for ValueOrVec { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + ValueOrVec::Single(value) => value.serialize(serializer), + ValueOrVec::Many(v) => { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry("many!", v)?; + map.end() + } + } + } +} + +impl<'de> Deserialize<'de> for ValueOrVec { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value = Value::deserialize(deserializer)?; + + match &value["many!"] { + Value::Array(arr) => Ok(ValueOrVec::Many(arr.to_vec())), + _ => Ok(ValueOrVec::Single(value)), + } + } +} + #[derive(Serialize, Deserialize, PartialEq, Debug)] struct TestCase { data: Value, - input: Option, + input: Option, modules: Vec, note: String, query: String, sort_bindings: Option, - want_result: Option, + want_result: Option, skip: Option, error: Option, traces: Option, @@ -285,6 +440,7 @@ fn yaml_test_impl(file: &str) -> Result<()> { let test: YamlTest = serde_yaml::from_str(&yaml_str)?; println!("running {file}"); + for case in test.cases { print!("case {} ", case.note); if case.skip == Some(true) { @@ -298,7 +454,7 @@ fn yaml_test_impl(file: &str) -> Result<()> { } let enable_tracing = case.traces.is_some() && case.traces.unwrap(); - // First eval the modules. + match eval_file( &case.modules, Some(case.data), @@ -307,7 +463,17 @@ fn yaml_test_impl(file: &str) -> Result<()> { enable_tracing, ) { Ok(results) => match case.want_result { - Some(want_result) => assert_match(results, want_result), + Some(want_result) => { + let mut expected_results = vec![]; + match want_result { + ValueOrVec::Single(single_result) => expected_results.push(single_result), + ValueOrVec::Many(mut many_result) => { + expected_results.append(&mut many_result) + } + } + + assert_match(results, expected_results); + } _ => panic!("eval succeeded and did not produce any errors"), }, Err(actual) => match &case.error {