Minimize PR 22 (#26)

* specific functions added to eval different rego components

Signed-off-by: eric-therond <eric.therond.fr@gmail.com>

* allow multiple inputs and results

Signed-off-by: eric-therond <eric.therond.fr@gmail.com>

* prepare_for_eval is necessary to be called

Signed-off-by: eric-therond <eric.therond.fr@gmail.com>

* test with the suggested code examples and clean scopes

Signed-off-by: eric-therond <eric.therond.fr@gmail.com>

* try to refactor first steps of evaluations

Signed-off-by: eric-therond <eric.therond.fr@gmail.com>

* improve coverage and fix clean state internal evaluation

Signed-off-by: eric-therond <eric.therond.fr@gmail.com>

* add getters and setters and fix clean function

Signed-off-by: eric-therond <eric.therond.fr@gmail.com>

* Tests are single input by default. Multi input specified via "many!" marker.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

---------

Signed-off-by: eric-therond <eric.therond.fr@gmail.com>
Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
Co-authored-by: eric-therond <eric.therond.fr@gmail.com>
This commit is contained in:
Anand Krishnamoorthi
2023-10-09 14:27:31 -07:00
committed by GitHub
parent 2ba718ba72
commit 2436467fbd
11 changed files with 430 additions and 66 deletions

View File

@@ -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<Scope>,
// 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<Value>, 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<Value>,
module: &'source Module<'source>,
rule: &'source Rule<'source>,
input: &Option<Value>,
enable_tracing: bool,
schedule: Option<&'source Schedule<'source>>,
) -> Result<Value> {
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<Value>,
) -> 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<Value>,
enable_tracing: bool,
) -> Result<Value> {
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<Value>, enable_tracing: bool) -> Result<Value> {
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<Value>,
input: &Option<Value>,
enable_tracing: bool,
schedule: Option<&'source Schedule<'source>>,
) -> Result<Value> {
self.prepare_for_eval(schedule, data)?;
self.eval_modules(input, enable_tracing)
}
pub fn eval_query_snippet(
&mut self,
snippet: &'source Expr<'source>,

View File

@@ -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)?,

View File

@@ -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)?,

View File

@@ -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:

View File

@@ -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)?,

View File

@@ -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(())
}

View File

@@ -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] ]

View File

@@ -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] ]

View File

@@ -5,4 +5,5 @@ mod arithmetic;
mod builtins;
mod compr;
mod r#in;
mod input;
mod variables;

View File

@@ -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)?,

View File

@@ -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<Value>, expected_results: Vec<Value>) {
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<Value>,
input: Option<Value>,
data_opt: Option<Value>,
input_opt: Option<ValueOrVec>,
query: &str,
enable_tracing: bool,
) -> Result<Value> {
) -> Result<Vec<Value>> {
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: "<query.rego>",
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<Value>,
input_opt: Option<ValueOrVec>,
query: &str,
enable_tracing: bool,
) -> Result<Vec<Value>> {
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: "<query.rego>",
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<Value>),
}
impl Serialize for ValueOrVec {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<D>(deserializer: D) -> Result<Self, D::Error>
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<Value>,
input: Option<ValueOrVec>,
modules: Vec<String>,
note: String,
query: String,
sort_bindings: Option<bool>,
want_result: Option<Value>,
want_result: Option<ValueOrVec>,
skip: Option<bool>,
error: Option<String>,
traces: Option<bool>,
@@ -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 {