use super::exprs::*; use super::*; use crate::rules::eval::operators::Comparator; use crate::rules::eval_context::{block_scope, ValueScope}; use crate::rules::path_value::compare_eq; use std::collections::HashMap; mod operators; fn exists_operation(value: &QueryResult) -> Result { Ok(match value { QueryResult::Resolved(_) | QueryResult::Literal(_) => true, QueryResult::UnResolved(_) => false, }) } fn element_empty_operation(value: &QueryResult) -> Result { let result = match value { QueryResult::Literal(value) | QueryResult::Resolved(value) => match &**value { PathAwareValue::List((_, list)) => list.is_empty(), PathAwareValue::Map((_, map)) => map.is_empty(), PathAwareValue::String((_, string)) => string.is_empty(), PathAwareValue::Bool((_, boolean)) => (*boolean).to_string().is_empty(), _ => { return Err(Error::IncompatibleError(format!( "Attempting EMPTY operation on type {} that does not support it at {}", value.type_info(), value.self_path() ))) } }, // // !EXISTS is the same as EMPTY // QueryResult::UnResolved(_) => true, }; Ok(result) } macro_rules! is_type_fn { ($name: ident, $type_: pat) => { fn $name(value: &QueryResult) -> Result { Ok(match value { QueryResult::Literal(resolved) | QueryResult::Resolved(resolved) => { match **resolved { $type_ => true, _ => false, } } QueryResult::UnResolved(_) => false, }) } }; } is_type_fn!(is_string_operation, PathAwareValue::String(_)); is_type_fn!(is_list_operation, PathAwareValue::List(_)); is_type_fn!(is_struct_operation, PathAwareValue::Map(_)); is_type_fn!(is_int_operation, PathAwareValue::Int(_)); #[cfg(test)] is_type_fn!(is_float_operation, PathAwareValue::Float(_)); is_type_fn!(is_bool_operation, PathAwareValue::Bool(_)); #[cfg(test)] is_type_fn!(is_char_range_operation, PathAwareValue::RangeChar(_)); #[cfg(test)] is_type_fn!(is_int_range_operation, PathAwareValue::RangeInt(_)); #[cfg(test)] is_type_fn!(is_float_range_operation, PathAwareValue::RangeFloat(_)); fn not_operation(operation: O) -> impl Fn(&QueryResult) -> Result where O: Fn(&QueryResult) -> Result, { move |value: &QueryResult| { Ok(match operation(value)? { true => false, false => true, }) } } fn inverse_operation(operation: O, inverse: bool) -> impl Fn(&QueryResult) -> Result where O: Fn(&QueryResult) -> Result, { move |value: &QueryResult| { Ok(match inverse { true => !operation(value)?, false => operation(value)?, }) } } #[allow(clippy::type_complexity)] fn record_unary_clause<'eval, 'value, 'loc: 'value, O>( operation: O, cmp: (CmpOperator, bool), context: String, custom_message: Option, eval_context: &'eval mut dyn EvalContext<'value, 'loc>, ) -> Box Result + 'eval> where O: Fn(&QueryResult) -> Result + 'eval, { Box::new(move |value: &QueryResult| { eval_context.start_record(&context)?; let mut check = ValueCheck { custom_message: custom_message.clone(), message: None, status: Status::PASS, from: value.clone(), }; match operation(value) { Ok(result) => { if !result { check.status = Status::FAIL; eval_context.end_record( &context, RecordType::ClauseValueCheck(ClauseCheck::Unary(UnaryValueCheck { value: check, comparison: cmp, })), )?; } else { eval_context .end_record(&context, RecordType::ClauseValueCheck(ClauseCheck::Success))?; } Ok(result) } Err(e) => { check.status = Status::FAIL; check.message = Some(format!("{}", e)); eval_context.end_record( &context, RecordType::ClauseValueCheck(ClauseCheck::Unary(UnaryValueCheck { value: check, comparison: cmp, })), )?; Err(e) } } }) } macro_rules! box_create_func { ($name: ident, $not: expr, $inverse: expr, $cmp: ident, $eval: ident, $cxt: ident, $msg: ident) => {{ { match $not { true => record_unary_clause( inverse_operation(not_operation($name), $inverse), $cmp, $cxt, $msg, $eval, ), false => { record_unary_clause(inverse_operation($name, $inverse), $cmp, $cxt, $msg, $eval) } } } }}; } pub(super) enum EvaluationResult { EmptyQueryResult(Status), QueryValueResult(Vec<(QueryResult, Status)>), } #[allow(clippy::type_complexity)] fn unary_operation<'r, 'l: 'r, 'loc: 'l>( lhs_query: &'l [QueryPart<'loc>], cmp: (CmpOperator, bool), inverse: bool, context: String, custom_message: Option, eval_context: &'r mut dyn EvalContext<'l, 'loc>, ) -> Result { let lhs = eval_context.query(lhs_query)?; // // Take care of the !empty clause without view projection, e.g. when checking %result !empty // That would translated to checking if each value was Resolved or UnResolved. If Resolved // then it is NOT EMPTY, if UnResolved it is EMPTY. // // NOTE: the check will pass the query for only one value resolved. Which is the correct behavior // For all the unresolved ones the individual clause associated will FAIL, this is the right // outcome. The earlier engine would suppress such a error and skip // let empty_on_expr = match &lhs_query[lhs_query.len() - 1] { QueryPart::Filter(_, _) | QueryPart::MapKeyFilter(_, _) => true, rest => rest.is_variable() && lhs_query.len() == 1, }; if empty_on_expr && cmp.0 == CmpOperator::Empty { return Ok({ if !lhs.is_empty() { let mut results = Vec::with_capacity(lhs.len()); for each in lhs { eval_context.start_record(&context)?; let (result, status) = match each { QueryResult::Literal(res) | QueryResult::Resolved(res) => { // // NULL == EMPTY // let status = if cmp.1 { // Not empty !res.is_null() } else { res.is_null() }; ( QueryResult::Resolved(res), match status { true => Status::PASS, // not_empty false => Status::FAIL, // fail not_empty }, ) } QueryResult::UnResolved(ur) => { ( QueryResult::UnResolved(ur), match cmp.1 { true => Status::FAIL, // !EXISTS == EMPTY, so !EMPTY == FAIL false => Status::PASS, // !EXISTS == EMPTY so PASS }, ) } }; let status = if inverse { match status { Status::PASS => Status::FAIL, Status::FAIL => Status::PASS, _ => unreachable!(), } } else { status }; match status { Status::PASS => { eval_context.end_record( &context, RecordType::ClauseValueCheck(ClauseCheck::Success), )?; } Status::FAIL => { eval_context.end_record( &context, RecordType::ClauseValueCheck(ClauseCheck::Unary(UnaryValueCheck { comparison: cmp, value: ValueCheck { status: Status::FAIL, message: None, custom_message: custom_message.clone(), from: result.clone(), }, })), )?; } _ => unreachable!(), } results.push((result, status)); } EvaluationResult::QueryValueResult(results) } else { EvaluationResult::EmptyQueryResult({ let result = !cmp.1; let result = if inverse { !result } else { result }; match result { true => { eval_context.start_record(&context)?; eval_context.end_record( &context, RecordType::ClauseValueCheck(ClauseCheck::Success), )?; Status::PASS } false => { eval_context.start_record(&context)?; eval_context.end_record( &context, RecordType::ClauseValueCheck(ClauseCheck::NoValueForEmptyCheck( custom_message, )), )?; Status::FAIL } } }) } }); } // // This only happens when the query has filters in them // if lhs.is_empty() { return Ok(EvaluationResult::EmptyQueryResult(Status::SKIP)); } use CmpOperator::*; let mut operation: Box Result> = match cmp { (CmpOperator::Exists, not_exists) => box_create_func!( exists_operation, not_exists, inverse, cmp, eval_context, context, custom_message ), (CmpOperator::Empty, not_empty) => box_create_func!( element_empty_operation, not_empty, inverse, cmp, eval_context, context, custom_message ), (CmpOperator::IsString, is_not_string) => box_create_func!( is_string_operation, is_not_string, inverse, cmp, eval_context, context, custom_message ), (CmpOperator::IsMap, is_not_map) => box_create_func!( is_struct_operation, is_not_map, inverse, cmp, eval_context, context, custom_message ), (CmpOperator::IsList, is_not_list) => box_create_func!( is_list_operation, is_not_list, inverse, cmp, eval_context, context, custom_message ), (CmpOperator::IsBool, is_not_bool) => box_create_func!( is_bool_operation, is_not_bool, inverse, cmp, eval_context, context, custom_message ), (CmpOperator::IsInt, is_not_int) => box_create_func!( is_int_operation, is_not_int, inverse, cmp, eval_context, context, custom_message ), (Eq | Gt | Ge | Lt | Le | In, _) => unreachable!(), }; let mut status = Vec::with_capacity(lhs.len()); for each in lhs { match (*operation)(&each)? { true => { status.push((each, Status::PASS)); } false => { status.push((each, Status::FAIL)); } } } Ok(EvaluationResult::QueryValueResult(status)) } enum ComparisonResult { Comparable(ComparisonWithRhs), NotComparable(NotComparableWithRhs), UnResolvedRhs(UnResolvedRhs), } struct LhsRhsPair { lhs: Rc, rhs: Rc, } struct ComparisonWithRhs { outcome: bool, pair: LhsRhsPair, } #[allow(dead_code)] struct NotComparableWithRhs { reason: String, pair: LhsRhsPair, } struct UnResolvedRhs { rhs: QueryResult, lhs: Rc, } fn each_lhs_compare( cmp: C, lhs: Rc, rhs: &[QueryResult], ) -> Result> where C: Fn(&PathAwareValue, &PathAwareValue) -> Result, { let mut statues = Vec::with_capacity(rhs.len()); for each_rhs in rhs { match each_rhs { QueryResult::Literal(each_rhs_resolved) | QueryResult::Resolved(each_rhs_resolved) => { match cmp(&lhs, each_rhs_resolved) { Ok(outcome) => { statues.push(ComparisonResult::Comparable(ComparisonWithRhs { outcome, pair: LhsRhsPair { lhs: Rc::clone(&lhs), rhs: Rc::clone(each_rhs_resolved), }, })); } Err(Error::NotComparable(reason)) => { if lhs.is_list() { // && each_rhs_resolved.is_scalar() { if let PathAwareValue::List((_, inner)) = &*lhs { for each in inner { match cmp(each, each_rhs_resolved) { Ok(outcome) => { statues.push(ComparisonResult::Comparable( ComparisonWithRhs { outcome, pair: LhsRhsPair { lhs: Rc::new(each.clone()), rhs: Rc::clone(each_rhs_resolved), }, }, )); } Err(Error::NotComparable(reason)) => { statues.push(ComparisonResult::NotComparable( NotComparableWithRhs { reason, pair: LhsRhsPair { lhs: Rc::new(each.clone()), rhs: Rc::clone(each_rhs_resolved), }, }, )); } Err(e) => return Err(e), } } continue; } } if lhs.is_scalar() { if let QueryResult::Literal(_) = each_rhs { if let PathAwareValue::List((_, rhs)) = &**each_rhs_resolved { if rhs.len() == 1 { let rhs_inner_single_element = &rhs[0]; match cmp(&lhs, rhs_inner_single_element) { Ok(outcome) => { statues.push(ComparisonResult::Comparable( ComparisonWithRhs { outcome, pair: LhsRhsPair { lhs: Rc::clone(&lhs), rhs: Rc::new( rhs_inner_single_element.clone(), ), }, }, )); } Err(Error::NotComparable(reason)) => { statues.push(ComparisonResult::NotComparable( NotComparableWithRhs { reason, pair: LhsRhsPair { lhs: Rc::clone(&lhs), rhs: Rc::new( rhs_inner_single_element.clone(), ), }, }, )); } Err(e) => return Err(e), } continue; } } } } statues.push(ComparisonResult::NotComparable(NotComparableWithRhs { reason, pair: LhsRhsPair { lhs: Rc::clone(&lhs), rhs: Rc::clone(each_rhs_resolved), }, })); } Err(e) => return Err(e), } } QueryResult::UnResolved(_ur) => { statues.push(ComparisonResult::UnResolvedRhs(UnResolvedRhs { rhs: each_rhs.clone(), lhs: Rc::clone(&lhs), })); } } } Ok(statues) } fn in_cmp(not_in: bool) -> impl Fn(&PathAwareValue, &PathAwareValue) -> Result { move |lhs, rhs| match (lhs, rhs) { (PathAwareValue::String((_, lhs_value)), PathAwareValue::String((_, rhs_value))) => { let result = rhs_value.contains(lhs_value); Ok(if not_in { !result } else { result }) } (_, PathAwareValue::List((_, rhs_list))) => Ok({ let mut tracking = Vec::with_capacity(rhs_list.len()); for each_rhs in rhs_list { tracking.push(compare_eq(lhs, each_rhs)?); } match tracking.iter().find(|s| **s) { Some(_) => !not_in, None => not_in, } }), (_, _) => { let result = compare_eq(lhs, rhs)?; Ok(if not_in { !result } else { result }) } } } fn report_value<'r, 'value: 'r, 'loc: 'value>( each_res: &ComparisonResult, cmp: (CmpOperator, bool), context: String, custom_message: Option, eval_context: &'r mut dyn EvalContext<'value, 'loc>, ) -> Result<(QueryResult, Status)> { let (lhs_value, rhs_value, outcome, reason) = match each_res { ComparisonResult::Comparable(ComparisonWithRhs { outcome, pair: LhsRhsPair { lhs: lhs_value, rhs: rhs_value, }, }) => ( QueryResult::Resolved(Rc::clone(lhs_value)), Some(QueryResult::Resolved(Rc::clone(rhs_value))), *outcome, None, ), //}, ComparisonResult::NotComparable(NotComparableWithRhs { pair: LhsRhsPair { rhs: rhs_value, lhs: lhs_value, }, .. }) => ( QueryResult::Resolved(Rc::clone(lhs_value)), Some(QueryResult::Resolved(Rc::clone(rhs_value))), false, None, ), // }, ComparisonResult::UnResolvedRhs(UnResolvedRhs { lhs: lhs_value, rhs: rhs_query_result, }) => ( QueryResult::Resolved(Rc::clone(lhs_value)), Some(rhs_query_result.clone()), false, None, ), // } }; Ok(if outcome { eval_context.start_record(&context)?; eval_context.end_record(&context, RecordType::ClauseValueCheck(ClauseCheck::Success))?; (lhs_value, Status::PASS) } else { eval_context.start_record(&context)?; eval_context.end_record( &context, RecordType::ClauseValueCheck(ClauseCheck::Comparison(ComparisonClauseCheck { from: lhs_value.clone(), comparison: cmp, to: rhs_value, custom_message, message: reason, status: Status::FAIL, })), )?; (lhs_value, Status::FAIL) }) } fn report_all_values<'r, 'value: 'r, 'loc: 'value>( comparisons: Vec, cmp: (CmpOperator, bool), context: String, custom_message: Option, eval_context: &'r mut dyn EvalContext<'value, 'loc>, ) -> Result> { let mut status = Vec::with_capacity(comparisons.len()); for each_res in comparisons { status.push(report_value( &each_res, cmp, context.clone(), custom_message.clone(), eval_context, )?); } Ok(status) } fn report_at_least_one<'r, 'value: 'r, 'loc: 'value>( rhs_comparisons: Vec, cmp: (CmpOperator, bool), context: String, custom_message: Option, eval_context: &'r mut dyn EvalContext<'value, 'loc>, ) -> Result> { let mut statues = Vec::with_capacity(rhs_comparisons.len()); let mut by_lhs_value = HashMap::new(); for each in &rhs_comparisons { match each { ComparisonResult::Comparable(ComparisonWithRhs { pair: LhsRhsPair { lhs, rhs }, .. }) => { by_lhs_value .entry(lhs) .or_insert(vec![]) .push((each, QueryResult::Resolved(Rc::clone(rhs)))); } ComparisonResult::NotComparable(NotComparableWithRhs { pair: LhsRhsPair { lhs, rhs }, .. }) => { by_lhs_value .entry(lhs) .or_insert(vec![]) .push((each, QueryResult::Resolved(Rc::clone(rhs)))); } ComparisonResult::UnResolvedRhs(UnResolvedRhs { rhs, lhs }) => { if let QueryResult::UnResolved(..) = rhs { by_lhs_value .entry(lhs) .or_insert(vec![]) .push((each, rhs.clone())); } } } } for (lhs, results) in by_lhs_value.iter() { let found = results.iter().find(|(r, _rhs)| { matches!( r, ComparisonResult::Comparable(ComparisonWithRhs { outcome: true, .. }) ) }); match found { Some(_) => { eval_context.start_record(&context)?; eval_context .end_record(&context, RecordType::ClauseValueCheck(ClauseCheck::Success))?; statues.push((QueryResult::Resolved(Rc::clone(lhs)), Status::PASS)) } None => { eval_context.start_record(&context)?; let to_collected = results .iter() .map(|(_, rhs)| rhs.clone()) .collect::>(); eval_context.end_record( &context, RecordType::ClauseValueCheck(ClauseCheck::InComparison(InComparisonCheck { from: QueryResult::Resolved(Rc::clone(lhs)), to: to_collected, message: None, custom_message: custom_message.clone(), status: Status::FAIL, comparison: cmp, })), )?; statues.push((QueryResult::Resolved(Rc::clone(lhs)), Status::FAIL)) } } } Ok(statues) } fn not_compare(cmp: O, invert: bool) -> impl Fn(&PathAwareValue, &PathAwareValue) -> Result where O: Fn(&PathAwareValue, &PathAwareValue) -> Result, { move |lhs, rhs| { let r = cmp(lhs, rhs)?; Ok(if invert { !r } else { r }) } } fn binary_operation<'value, 'loc: 'value>( lhs_query: &'value [QueryPart<'loc>], rhs: &[QueryResult], cmp: (CmpOperator, bool), context: String, custom_message: Option, eval_context: &mut dyn EvalContext<'value, 'loc>, ) -> Result { let lhs = eval_context.query(lhs_query)?; let results = cmp.compare(&lhs, rhs)?; match results { operators::EvalResult::Skip => Ok(EvaluationResult::EmptyQueryResult(Status::SKIP)), operators::EvalResult::Result(results) => { let mut statues: Vec<(QueryResult, Status)> = Vec::with_capacity(lhs.len()); for each in results { match each { operators::ValueEvalResult::LhsUnresolved(ur) => { eval_context.start_record(&context)?; eval_context.end_record( &context, RecordType::ClauseValueCheck(ClauseCheck::Comparison( ComparisonClauseCheck { status: Status::FAIL, message: None, custom_message: custom_message.clone(), comparison: cmp, from: QueryResult::UnResolved(ur.clone()), to: None, }, )), )?; statues.push((QueryResult::UnResolved(ur), Status::FAIL)); } operators::ValueEvalResult::ComparisonResult( operators::ComparisonResult::RhsUnresolved(urhs, lhs), ) => { eval_context.start_record(&context)?; eval_context.end_record( &context, RecordType::ClauseValueCheck(ClauseCheck::Comparison( ComparisonClauseCheck { status: Status::FAIL, message: None, custom_message: custom_message.clone(), comparison: cmp, from: QueryResult::Resolved(Rc::clone(&lhs)), to: Some(QueryResult::UnResolved(urhs)), }, )), )?; statues.push((QueryResult::Resolved(Rc::clone(&lhs)), Status::FAIL)); } operators::ValueEvalResult::ComparisonResult( operators::ComparisonResult::NotComparable(nc), ) => { eval_context.start_record(&context)?; eval_context.end_record( &context, RecordType::ClauseValueCheck(ClauseCheck::Comparison( ComparisonClauseCheck { status: Status::FAIL, message: Some(nc.reason), custom_message: custom_message.clone(), comparison: cmp, from: QueryResult::Resolved(Rc::clone(&nc.pair.lhs)), to: Some(QueryResult::Resolved(nc.pair.rhs)), }, )), )?; statues.push((QueryResult::Resolved(nc.pair.lhs), Status::FAIL)); } operators::ValueEvalResult::ComparisonResult( operators::ComparisonResult::Success(cmp), ) => match cmp { operators::Compare::ListIn(lin) => { eval_context.start_record(&context)?; eval_context.end_record( &context, RecordType::ClauseValueCheck(ClauseCheck::Success), )?; statues.push((QueryResult::Resolved(lin.lhs), Status::PASS)); } operators::Compare::QueryIn(qin) => { for each in qin.lhs { eval_context.start_record(&context)?; eval_context.end_record( &context, RecordType::ClauseValueCheck(ClauseCheck::Success), )?; statues.push((QueryResult::Resolved(each), Status::PASS)); } } operators::Compare::Value(pair) => { eval_context.start_record(&context)?; eval_context.end_record( &context, RecordType::ClauseValueCheck(ClauseCheck::Success), )?; statues.push((QueryResult::Resolved(pair.lhs), Status::PASS)); } operators::Compare::ValueIn(val) => { eval_context.start_record(&context)?; eval_context.end_record( &context, RecordType::ClauseValueCheck(ClauseCheck::Success), )?; statues.push((QueryResult::Resolved(val.lhs), Status::PASS)); } }, operators::ValueEvalResult::ComparisonResult( operators::ComparisonResult::Fail(cmpr), ) => match cmpr { operators::Compare::Value(pair) => { eval_context.start_record(&context)?; eval_context.end_record( &context, RecordType::ClauseValueCheck(ClauseCheck::Comparison( ComparisonClauseCheck { status: Status::FAIL, message: None, custom_message: custom_message.clone(), comparison: cmp, from: QueryResult::Resolved(Rc::clone(&pair.lhs)), to: Some(QueryResult::Resolved(pair.rhs)), }, )), )?; statues .push((QueryResult::Resolved(Rc::clone(&pair.lhs)), Status::FAIL)); } operators::Compare::ValueIn(pair) => { eval_context.start_record(&context)?; eval_context.end_record( &context, RecordType::ClauseValueCheck(ClauseCheck::InComparison( InComparisonCheck { status: Status::FAIL, message: None, custom_message: custom_message.clone(), comparison: cmp, from: QueryResult::Resolved(Rc::clone(&pair.lhs)), to: vec![QueryResult::Resolved(pair.rhs)], }, )), )?; statues .push((QueryResult::Resolved(Rc::clone(&pair.lhs)), Status::FAIL)); } operators::Compare::ListIn(lin) => { eval_context.start_record(&context)?; eval_context.end_record( &context, RecordType::ClauseValueCheck(ClauseCheck::InComparison( InComparisonCheck { status: Status::FAIL, message: None, custom_message: custom_message.clone(), comparison: cmp, from: QueryResult::Resolved(Rc::clone(&lin.lhs)), to: vec![QueryResult::Resolved(lin.rhs)], }, )), )?; statues .push((QueryResult::Resolved(Rc::clone(&lin.lhs)), Status::FAIL)); } operators::Compare::QueryIn(qin) => { let rhs = qin .rhs .iter() .cloned() .map(QueryResult::Resolved) .collect::>(); for lhs in qin.diff { eval_context.start_record(&context)?; eval_context.end_record( &context, RecordType::ClauseValueCheck(ClauseCheck::InComparison( InComparisonCheck { status: Status::FAIL, message: None, custom_message: custom_message.clone(), comparison: cmp, from: QueryResult::Resolved(Rc::clone(&lhs)), to: rhs.clone(), }, )), )?; statues .push((QueryResult::Resolved(Rc::clone(&lhs)), Status::FAIL)); } } }, } } Ok(EvaluationResult::QueryValueResult(statues)) } } } pub(super) fn real_binary_operation<'value, 'loc: 'value>( lhs: &[QueryResult], rhs: &[QueryResult], cmp: (CmpOperator, bool), context: String, custom_message: Option, eval_context: &mut dyn EvalContext<'value, 'loc>, ) -> Result { let mut statues: Vec<(QueryResult, Status)> = Vec::with_capacity(lhs.len()); let cmp = if cmp.0 == CmpOperator::Eq && rhs.len() > 1 { (CmpOperator::In, cmp.1) } else { cmp }; for each in lhs.iter() { match each { QueryResult::UnResolved(_ur) => { eval_context.start_record(&context)?; eval_context.end_record( &context, RecordType::ClauseValueCheck(ClauseCheck::Comparison(ComparisonClauseCheck { status: Status::FAIL, message: None, custom_message: custom_message.clone(), comparison: cmp, from: each.clone(), to: None, })), )?; statues.push((each.clone(), Status::FAIL)); } QueryResult::Literal(l) | QueryResult::Resolved(l) => { let r = match cmp { (CmpOperator::Eq, is_not) => each_lhs_compare( not_compare(crate::rules::path_value::compare_eq, is_not), Rc::clone(l), rhs, )?, (CmpOperator::Ge, is_not) => each_lhs_compare( not_compare(crate::rules::path_value::compare_ge, is_not), Rc::clone(l), rhs, )?, (CmpOperator::Gt, is_not) => each_lhs_compare( not_compare(crate::rules::path_value::compare_gt, is_not), Rc::clone(l), rhs, )?, (CmpOperator::Lt, is_not) => each_lhs_compare( not_compare(crate::rules::path_value::compare_lt, is_not), Rc::clone(l), rhs, )?, (CmpOperator::Le, is_not) => each_lhs_compare( not_compare(crate::rules::path_value::compare_le, is_not), Rc::clone(l), rhs, )?, (CmpOperator::In, is_not) => { each_lhs_compare(in_cmp(is_not), Rc::clone(l), rhs)? } _ => unreachable!(), }; match cmp.0 { CmpOperator::In => { statues.extend(report_at_least_one( r, cmp, context.clone(), custom_message.clone(), eval_context, )?); } _ => { let status = report_all_values( r, cmp, context.clone(), custom_message.clone(), eval_context, )?; statues.extend(status); } } } }; } Ok(EvaluationResult::QueryValueResult(statues)) } #[allow(clippy::never_loop)] pub(in crate::rules) fn eval_guard_access_clause<'value, 'loc: 'value>( gac: &'value GuardAccessClause<'loc>, resolver: &mut dyn EvalContext<'value, 'loc>, ) -> Result { let all = gac.access_clause.query.match_all; let blk_context = format!("GuardAccessClause#block{}", gac); resolver.start_record(&blk_context)?; let statues = if gac.access_clause.comparator.0.is_unary() { unary_operation( &gac.access_clause.query.query, gac.access_clause.comparator, gac.negation, format!("{}", gac), gac.access_clause.custom_message.clone(), resolver, ) } else { let (rhs, _) = match &gac.access_clause.compare_with { Some(val) => match val { LetValue::Value(rhs_val) => { (vec![QueryResult::Literal(Rc::new(rhs_val.clone()))], true) } LetValue::AccessClause(acc_querty) => match resolver.query(&acc_querty.query) { Ok(result) => (result, false), Err(e) => { resolver.end_record( &blk_context, RecordType::GuardClauseBlockCheck(BlockCheck { status: Status::FAIL, at_least_one_matches: !all, message: Some(format!("Error {e} when handling clause, bailing")), }), )?; return Err(e); } }, LetValue::FunctionCall(_) => todo!(), }, None => { resolver.end_record( &blk_context, RecordType::GuardClauseBlockCheck(BlockCheck { status: Status::FAIL, at_least_one_matches: !all, message: Some( "Error not RHS for binary clause when handling clause, bailing" .to_string(), ), }), )?; return Err(Error::NotComparable(format!( "GuardAccessClause {}, did not have a RHS for compare operation", blk_context ))); } }; binary_operation( &gac.access_clause.query.query, &rhs, gac.access_clause.comparator, format!("{}", gac), gac.access_clause.custom_message.clone(), resolver, ) }; match statues { Ok(statues) => match statues { EvaluationResult::EmptyQueryResult(status) => { resolver.end_record( &blk_context, RecordType::GuardClauseBlockCheck(BlockCheck { status, message: None, at_least_one_matches: all, }), )?; Ok(status) } EvaluationResult::QueryValueResult(result) => { let outcome = loop { let mut fails = 0; let mut pass = 0; for (_value, status) in result { match status { Status::PASS => { pass += 1; } Status::FAIL => { fails += 1; } Status::SKIP => unreachable!(), } } if all { if fails > 0 { break Status::FAIL; } break Status::PASS; } else { if pass > 0 { break Status::PASS; } break Status::FAIL; } }; resolver.end_record( &blk_context, RecordType::GuardClauseBlockCheck(BlockCheck { message: None, status: outcome, at_least_one_matches: !all, }), )?; Ok(outcome) } }, Err(e) => { resolver.end_record( &blk_context, RecordType::GuardClauseBlockCheck(BlockCheck { status: Status::FAIL, at_least_one_matches: !all, message: Some(format!("Error {} when handling clause, bailing", e)), }), )?; Err(e) } } } pub(in crate::rules) fn eval_guard_named_clause<'value, 'loc: 'value>( gnc: &'value GuardNamedRuleClause<'loc>, resolver: &mut dyn EvalContext<'value, 'loc>, ) -> Result { let context = format!("{}", gnc); resolver.start_record(&context)?; match resolver.rule_status(&gnc.dependent_rule) { Ok(status) => { let status = match status { Status::PASS => { if gnc.negation { Status::FAIL } else { Status::PASS } } _ => { if gnc.negation { Status::PASS } else { Status::FAIL } } }; match status { Status::PASS => { resolver .end_record(&context, RecordType::ClauseValueCheck(ClauseCheck::Success))?; } Status::FAIL => { resolver.end_record( &context, RecordType::ClauseValueCheck(ClauseCheck::DependentRule( MissingValueCheck { rule: &gnc.dependent_rule, status: Status::FAIL, message: None, custom_message: gnc.custom_message.clone(), }, )), )?; } _ => unreachable!(), } Ok(status) } Err(e) => { resolver.end_record( &context, RecordType::ClauseValueCheck(ClauseCheck::DependentRule(MissingValueCheck { rule: &gnc.dependent_rule, status: Status::FAIL, message: Some(format!("{} failed due to error {}", context, e)), custom_message: gnc.custom_message.clone(), })), )?; Err(e) } } } pub(in crate::rules) fn eval_general_block_clause<'value, 'loc: 'value, T, E>( block: &'value Block<'loc, T>, resolver: &mut dyn EvalContext<'value, 'loc>, eval_fn: E, ) -> Result where E: Fn(&'value T, &mut dyn EvalContext<'value, 'loc>) -> Result, { let mut block_scope = block_scope(block, resolver.root(), resolver)?; eval_conjunction_clauses(&block.conjunctions, &mut block_scope, eval_fn) } pub(in crate::rules) fn eval_guard_block_clause<'value, 'loc: 'value>( block_clause: &'value BlockGuardClause<'loc>, resolver: &mut dyn EvalContext<'value, 'loc>, ) -> Result { let context = format!("BlockGuardClause#{}", block_clause.location); let match_all = block_clause.query.match_all; resolver.start_record(&context)?; let block_values = match resolver.query(&block_clause.query.query) { Ok(values) => values, Err(e) => { resolver.end_record( &context, RecordType::BlockGuardCheck(BlockCheck { status: Status::FAIL, at_least_one_matches: !match_all, message: None, }), )?; return Err(e); } }; if block_values.is_empty() { let status = if block_clause.not_empty { Status::FAIL } else { Status::SKIP }; resolver.end_record( &context, RecordType::BlockGuardCheck(BlockCheck { status, at_least_one_matches: !match_all, message: None, }), )?; return Ok(status); } let mut fails = 0; let mut passes = 0; for each in block_values { match each { QueryResult::UnResolved(ur) => { fails += 1; let guard_cxt = format!("GuardBlockAccessClause#{}", block_clause.location); resolver.start_record(&guard_cxt)?; resolver.end_record( &guard_cxt, RecordType::ClauseValueCheck(ClauseCheck::MissingBlockValue(ValueCheck { message: Some(format!( "Query {} did not resolve to correct value, reason {}", SliceDisplay(&block_clause.query.query), ur.reason.as_ref().map_or("", |s| s) )), status: Status::FAIL, custom_message: None, from: QueryResult::UnResolved(ur), })), )?; } QueryResult::Literal(rv) | QueryResult::Resolved(rv) => { let mut val_resolver = ValueScope { root: rv, parent: resolver, }; match eval_general_block_clause( &block_clause.block, &mut val_resolver, eval_guard_clause, ) { Ok(status) => match status { Status::PASS => { passes += 1; } Status::FAIL => { fails += 1; } Status::SKIP => {} }, Err(e) => { resolver.end_record( &context, RecordType::BlockGuardCheck(BlockCheck { status: Status::FAIL, at_least_one_matches: !match_all, message: Some(format!( "Error {} when handling block clause, bailing", e )), }), )?; return Err(e); } } } } } let status = if match_all { if fails > 0 { Status::FAIL } else if passes > 0 { Status::PASS } else { Status::SKIP } } else if passes > 0 { Status::PASS } else if fails > 0 { Status::FAIL } else { Status::SKIP }; resolver.end_record( &context, RecordType::BlockGuardCheck(BlockCheck { status, at_least_one_matches: !match_all, message: None, }), )?; Ok(status) } fn eval_when_condition_block<'value, 'loc: 'value>( context: String, conditions: &'value WhenConditions<'loc>, block: &'value Block<'loc, GuardClause<'loc>>, resolver: &mut dyn EvalContext<'value, 'loc>, ) -> Result { resolver.start_record(&context)?; let when_context = format!("{}/When", context); resolver.start_record(&when_context)?; let block = match eval_conjunction_clauses(conditions, resolver, eval_when_clause) { Ok(status) => { if status != Status::PASS { resolver.end_record(&when_context, RecordType::WhenCondition(status))?; resolver.end_record( &context, RecordType::WhenCheck(BlockCheck { status: Status::SKIP, at_least_one_matches: false, message: None, }), )?; return Ok(Status::SKIP); } resolver.end_record(&when_context, RecordType::WhenCondition(Status::PASS))?; block } Err(e) => { resolver.end_record(&when_context, RecordType::WhenCondition(Status::FAIL))?; resolver.end_record( &context, RecordType::WhenCheck(BlockCheck { status: Status::FAIL, message: Some(format!( "Error {} during type condition evaluation, bailing", e )), at_least_one_matches: false, }), )?; return Err(e); } }; Ok( match eval_general_block_clause(block, resolver, eval_guard_clause) { Ok(status) => { resolver.end_record( &context, RecordType::WhenCheck(BlockCheck { status, message: None, at_least_one_matches: false, }), )?; status } Err(e) => { resolver.end_record( &context, RecordType::WhenCheck(BlockCheck { status: Status::FAIL, message: Some(format!( "Error {} during type condition evaluation, bailing", e )), at_least_one_matches: false, }), )?; return Err(e); } }, ) } struct ResolvedParameterContext<'eval, 'value, 'loc: 'value> { call_rule: &'value ParameterizedNamedRuleClause<'loc>, resolved_parameters: HashMap<&'value str, Vec>, parent: &'eval mut dyn EvalContext<'value, 'loc>, } impl<'eval, 'value, 'loc: 'value> EvalContext<'value, 'loc> for ResolvedParameterContext<'eval, 'value, 'loc> { fn query(&mut self, query: &'value [QueryPart<'loc>]) -> Result> { self.parent.query(query) } fn find_parameterized_rule( &mut self, rule_name: &str, ) -> Result<&'value ParameterizedRule<'loc>> { self.parent.find_parameterized_rule(rule_name) } fn root(&mut self) -> Rc { self.parent.root() } fn rule_status(&mut self, rule_name: &'value str) -> Result { self.parent.rule_status(rule_name) } fn resolve_variable(&mut self, variable_name: &'value str) -> Result> { match self.resolved_parameters.get(variable_name) { Some(res) => Ok(res.clone()), None => self.parent.resolve_variable(variable_name), } } fn add_variable_capture_key( &mut self, variable_name: &'value str, key: Rc, ) -> Result<()> { self.parent.add_variable_capture_key(variable_name, key) } } impl<'eval, 'value, 'loc: 'value> RecordTracer<'value> for ResolvedParameterContext<'eval, 'value, 'loc> { fn start_record(&mut self, context: &str) -> Result<()> { self.parent.start_record(context) } fn end_record(&mut self, context: &str, record: RecordType<'value>) -> Result<()> { let record = match record { RecordType::RuleCheck(ns) => { if ns.name == self.call_rule.named_rule.dependent_rule { RecordType::RuleCheck(NamedStatus { name: ns.name, status: ns.status, message: self.call_rule.named_rule.custom_message.clone(), }) } else { RecordType::RuleCheck(ns) } } rest => rest, }; self.parent.end_record(context, record) } } pub(in crate::rules) fn eval_parameterized_rule_call<'value, 'loc: 'value>( call_rule: &'value ParameterizedNamedRuleClause<'loc>, resolver: &mut dyn EvalContext<'value, 'loc>, ) -> Result { let param_rule = resolver.find_parameterized_rule(&call_rule.named_rule.dependent_rule)?; if param_rule.parameter_names.len() != call_rule.parameters.len() { return Err(Error::IncompatibleError(format!( "Arity mismatch for called parameter rule {}, expected {}, got {}", call_rule.named_rule.dependent_rule, param_rule.parameter_names.len(), call_rule.parameters.len() ))); } let mut resolved_parameters = HashMap::with_capacity(call_rule.parameters.len()); for (idx, each) in call_rule.parameters.iter().enumerate() { match each { LetValue::Value(val) => { resolved_parameters.insert( (param_rule.parameter_names[idx]).as_str(), vec![QueryResult::Resolved(Rc::new(val.clone()))], ); } LetValue::AccessClause(query) => { resolved_parameters.insert( (param_rule.parameter_names[idx]).as_str(), resolver.query(&query.query)?, ); } // TODO: when we add inline function support LetValue::FunctionCall(_) => unimplemented!(), } } let mut eval = ResolvedParameterContext { parent: resolver, resolved_parameters, call_rule, }; eval_rule(¶m_rule.rule, &mut eval) } pub(in crate::rules) fn eval_guard_clause<'value, 'loc: 'value>( gc: &'value GuardClause<'loc>, resolver: &mut dyn EvalContext<'value, 'loc>, ) -> Result { match gc { GuardClause::Clause(gac) => eval_guard_access_clause(gac, resolver), GuardClause::NamedRule(gnc) => eval_guard_named_clause(gnc, resolver), GuardClause::BlockClause(bc) => eval_guard_block_clause(bc, resolver), GuardClause::WhenBlock(conditions, block) => eval_when_condition_block( "GuardConditionClause".to_string(), conditions, block, resolver, ), GuardClause::ParameterizedNamedRule(prc) => eval_parameterized_rule_call(prc, resolver), } } pub(in crate::rules) fn eval_when_clause<'value, 'loc: 'value>( when_clause: &'value WhenGuardClause<'loc>, resolver: &mut dyn EvalContext<'value, 'loc>, ) -> Result { match when_clause { WhenGuardClause::Clause(gac) => eval_guard_access_clause(gac, resolver), WhenGuardClause::NamedRule(gnr) => eval_guard_named_clause(gnr, resolver), WhenGuardClause::ParameterizedNamedRule(prc) => eval_parameterized_rule_call(prc, resolver), } } pub(in crate::rules) fn eval_type_block_clause<'value, 'loc: 'value>( type_block: &'value TypeBlock<'loc>, resolver: &mut dyn EvalContext<'value, 'loc>, ) -> Result { let context = format!("TypeBlock#{}", type_block.type_name); resolver.start_record(&context)?; let block = if let Some(conditions) = &type_block.conditions { let when_context = format!("TypeBlock#{}/When", type_block.type_name); resolver.start_record(&when_context)?; match eval_conjunction_clauses(conditions, resolver, eval_when_clause) { Ok(status) => { if status != Status::PASS { resolver.end_record(&when_context, RecordType::TypeCondition(status))?; resolver.end_record( &context, RecordType::TypeCheck(TypeBlockCheck { type_name: &type_block.type_name, block: BlockCheck { status: Status::SKIP, at_least_one_matches: false, message: None, }, }), )?; return Ok(Status::SKIP); } resolver.end_record(&when_context, RecordType::TypeCondition(Status::PASS))?; &type_block.block } Err(e) => { resolver.end_record(&when_context, RecordType::TypeCondition(Status::FAIL))?; resolver.end_record( &context, RecordType::TypeCheck(TypeBlockCheck { type_name: &type_block.type_name, block: BlockCheck { status: Status::FAIL, message: Some(format!( "Error {} during type condition evaluation, bailing", e )), at_least_one_matches: false, }, }), )?; return Err(e); } } } else { &type_block.block }; let values = match resolver.query(&type_block.query) { Ok(values) => values, Err(e) => { resolver.end_record( &context, RecordType::TypeCheck(TypeBlockCheck { type_name: &type_block.type_name, block: BlockCheck { status: Status::FAIL, at_least_one_matches: false, message: None, }, }), )?; return Err(e); } }; if values.is_empty() { resolver.end_record( &context, RecordType::TypeCheck(TypeBlockCheck { type_name: &type_block.type_name, block: BlockCheck { status: Status::SKIP, at_least_one_matches: false, message: None, }, }), )?; return Ok(Status::SKIP); } let mut fails = 0; let mut passes = 0; for (idx, each) in values.iter().enumerate() { match each { QueryResult::Literal(rv) | QueryResult::Resolved(rv) => { let block_context = format!("{}/{}", context, idx); resolver.start_record(&block_context)?; let mut val_resolver = ValueScope { root: Rc::clone(rv), parent: resolver, }; match eval_general_block_clause(block, &mut val_resolver, eval_guard_clause) { Ok(status) => { match status { Status::PASS => { passes += 1; } Status::FAIL => { fails += 1; } Status::SKIP => {} } resolver.end_record(&block_context, RecordType::TypeBlock(status))?; } Err(e) => { resolver.end_record(&block_context, RecordType::TypeBlock(Status::FAIL))?; resolver.end_record( &context, RecordType::TypeCheck(TypeBlockCheck { type_name: &type_block.type_name, block: BlockCheck { status: Status::FAIL, message: Some(format!( "Error {} during type block evaluation, bailing", e )), at_least_one_matches: false, }, }), )?; return Err(e); } } } QueryResult::UnResolved(_) => unreachable!(), } } let status = if fails > 0 { Status::FAIL } else if passes > 0 { Status::PASS } else { Status::SKIP }; resolver.end_record( &context, RecordType::TypeCheck(TypeBlockCheck { type_name: &type_block.type_name, block: BlockCheck { status, message: None, at_least_one_matches: false, }, }), )?; Ok(status) } pub(in crate::rules) fn eval_rule_clause<'value, 'loc: 'value>( rule_clause: &'value RuleClause<'loc>, resolver: &mut dyn EvalContext<'value, 'loc>, ) -> Result { match rule_clause { RuleClause::Clause(gc) => eval_guard_clause(gc, resolver), RuleClause::TypeBlock(tb) => eval_type_block_clause(tb, resolver), RuleClause::WhenBlock(conditions, block) => { eval_when_condition_block("RuleClause".to_string(), conditions, block, resolver) } } } pub(in crate::rules) fn eval_rule<'value, 'loc: 'value>( rule: &'value Rule<'loc>, resolver: &mut dyn EvalContext<'value, 'loc>, ) -> Result { let context = rule.rule_name.to_string(); resolver.start_record(&context)?; let block = if let Some(conditions) = &rule.conditions { let when_context = format!("Rule#{}/When", context); resolver.start_record(&when_context)?; match eval_conjunction_clauses(conditions, resolver, eval_when_clause) { Ok(status) => { if status != Status::PASS { resolver.end_record(&when_context, RecordType::RuleCondition(status))?; resolver.end_record( &context, RecordType::RuleCheck(NamedStatus { status: Status::SKIP, name: &rule.rule_name, ..Default::default() }), )?; return Ok(Status::SKIP); } resolver.end_record(&when_context, RecordType::RuleCondition(Status::PASS))?; &rule.block } Err(e) => { resolver.end_record(&when_context, RecordType::RuleCondition(Status::FAIL))?; resolver.end_record( &context, RecordType::RuleCheck(NamedStatus { status: Status::FAIL, name: &rule.rule_name, ..Default::default() }), )?; return Err(e); } } } else { &rule.block }; match eval_general_block_clause(block, resolver, eval_rule_clause) { Ok(status) => { resolver.end_record( &context, RecordType::RuleCheck(NamedStatus { status, name: &rule.rule_name, ..Default::default() }), )?; Ok(status) } Err(e) => { resolver.end_record( &context, RecordType::RuleCheck(NamedStatus { status: Status::FAIL, name: &rule.rule_name, ..Default::default() }), )?; Err(e) } } } impl<'loc> std::fmt::Display for RulesFile<'loc> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_fmt(format_args!("File(rules={})", self.guard_rules.len()))?; Ok(()) } } pub(crate) fn eval_rules_file<'value, 'loc: 'value>( rule: &'value RulesFile<'loc>, resolver: &mut dyn EvalContext<'value, 'loc>, data_file_name: Option<&'value str>, ) -> Result { let context = format!("{}", rule); resolver.start_record(&context)?; let mut fails = 0; let mut passes = 0; for each_rule in &rule.guard_rules { match eval_rule(each_rule, resolver) { Ok(status) => match status { Status::PASS => { passes += 1; } Status::FAIL => { fails += 1; } Status::SKIP => {} }, Err(e) => { resolver.end_record( &context, RecordType::RuleCheck(NamedStatus { status: Status::FAIL, name: &each_rule.rule_name, ..Default::default() }), )?; return Err(e); } } } let overall = if fails > 0 { Status::FAIL } else if passes > 0 { Status::PASS } else { Status::SKIP }; resolver.end_record( &context, RecordType::FileCheck(NamedStatus { status: overall, name: data_file_name.unwrap_or_default(), ..Default::default() }), )?; Ok(overall) } #[allow(clippy::never_loop)] pub(in crate::rules) fn eval_conjunction_clauses<'value, 'loc: 'value, T, E>( conjunctions: &'value Conjunctions, resolver: &mut dyn EvalContext<'value, 'loc>, eval_fn: E, ) -> Result where E: Fn(&'value T, &mut dyn EvalContext<'value, 'loc>) -> Result, { Ok(loop { let mut num_passes = 0; let mut num_fails = 0; let context = format!("{}#disjunction", std::any::type_name::()); 'conjunction: for conjunction in conjunctions { let mut num_of_disjunction_fails = 0; let multiple_ors_present = conjunction.len() > 1; if multiple_ors_present { resolver.start_record(&context)?; } for disjunction in conjunction { match eval_fn(disjunction, resolver) { Ok(status) => match status { Status::PASS => { num_passes += 1; if multiple_ors_present { resolver.end_record( &context, RecordType::Disjunction(BlockCheck { message: None, at_least_one_matches: true, status: Status::PASS, }), )?; } continue 'conjunction; } Status::SKIP => {} Status::FAIL => { num_of_disjunction_fails += 1; } }, Err(e) => { if multiple_ors_present { resolver.end_record( &context, RecordType::Disjunction(BlockCheck { message: Some(format!( "Disjunction failed due to error {}, bailing", e )), status: Status::FAIL, at_least_one_matches: true, }), )?; } return Err(e); } } } if num_of_disjunction_fails > 0 { num_fails += 1; } if multiple_ors_present { if num_of_disjunction_fails > 0 { resolver.end_record( &context, RecordType::Disjunction(BlockCheck { message: None, status: Status::FAIL, at_least_one_matches: true, }), )?; } else { resolver.end_record( &context, RecordType::Disjunction(BlockCheck { message: None, status: Status::SKIP, at_least_one_matches: true, }), )?; } } } if num_fails > 0 { break Status::FAIL; } if num_passes > 0 { break Status::PASS; } break Status::SKIP; }) } #[cfg(test)] #[path = "eval_tests.rs"] mod eval_tests;