zen-engine 2.0.1

Business rules engine
Documentation
use std::sync::Arc;

use ahash::HashSet;
use serde::{Deserialize, Serialize};
use zen_expression::intellisense::IntelliSense;
use zen_expression::variable::{Variable, VariableType};
use zen_expression::Isolate;

use crate::workspace::types::{
    BlockTrace, ConditionTrace, Cursor, CursorTarget, Diagnostic, DiagnosticCode, ExpressionKind,
    NlExpression,
};

use crate::policy::ArcStrTrim;

use super::context::{AnalysisContext, ExecutionContext, ExecutionError};
use super::{
    Block, BlockKind, BlockReadPlan, ConditionalReads, ExpressionLocation, ParseContext,
    ReadFlattenFn, WriteSite, WriteTarget,
};

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AssertionDoc {
    pub output: Arc<str>,
    pub conditions: Vec<ConditionDoc>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ConditionDoc {
    pub id: Arc<str>,
    pub expression: Arc<str>,
    pub operator: ConditionOperatorDoc,
    #[serde(default)]
    pub depth: u32,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum ConditionOperatorDoc {
    And,
    Or,
}

#[derive(Debug, Clone)]
pub struct AssertionIr {
    pub output: Arc<str>,
    pub conditions: Vec<AssertionCondition>,
}

#[derive(Debug, Clone)]
pub struct AssertionCondition {
    pub id: Arc<str>,
    pub expression: Arc<str>,
    pub operator: ConditionOperator,
    pub depth: u32,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConditionOperator {
    And,
    Or,
}

use crate::policy::MAX_RECURSION_DEPTH;

impl ConditionOperator {
    fn apply(&self, left: bool, right: bool) -> bool {
        match self {
            ConditionOperator::And => left && right,
            ConditionOperator::Or => left || right,
        }
    }
}

impl AssertionIr {
    pub(crate) fn parse(
        id: &Arc<str>,
        doc: &AssertionDoc,
        policy_path: &Arc<str>,
        diagnostics: &mut Vec<Diagnostic>,
    ) -> Block {
        let mut cx = ParseContext {
            policy_path,
            block_id: id,
            diagnostics,
        };
        let mut output = doc.output.trimmed();

        if output.is_empty() {
            cx.block_warning(
                DiagnosticCode::InvalidWritePath,
                "assertion has no output property path",
            );
        } else if let Err(reason) = WriteTarget::validate_path(&output) {
            cx.block_error(
                DiagnosticCode::InvalidWritePath,
                format!("invalid write path '{output}': {reason}"),
            );
            output = Arc::from("");
        }

        if doc.conditions.is_empty() {
            cx.block_error(DiagnosticCode::EmptyBlock, "assertion has no conditions");
        }

        let conditions: Vec<_> = doc
            .conditions
            .iter()
            .filter_map(|c| {
                let expr = c.expression.trimmed();
                if expr.is_empty() {
                    cx.expression_warning(
                        &c.id,
                        DiagnosticCode::EmptyBlock,
                        "assertion condition is empty",
                    );
                    return None;
                }
                if (c.depth as usize) >= MAX_RECURSION_DEPTH {
                    cx.expression_error(
                        &c.id,
                        DiagnosticCode::MaxDepthExceeded,
                        format!(
                            "assertion condition depth {} exceeds maximum of {}",
                            c.depth, MAX_RECURSION_DEPTH
                        ),
                    );
                    return None;
                }
                Some(AssertionCondition {
                    id: c.id.clone(),
                    expression: expr,
                    operator: match c.operator {
                        ConditionOperatorDoc::And => ConditionOperator::And,
                        ConditionOperatorDoc::Or => ConditionOperator::Or,
                    },
                    depth: c.depth,
                })
            })
            .collect();

        Block {
            id: id.clone(),
            kind: BlockKind::Assertion(Arc::new(AssertionIr { output, conditions })),
        }
    }

    pub(super) fn expressions(&self, block_id: &Arc<str>) -> Vec<ExpressionLocation> {
        self.conditions
            .iter()
            .filter_map(|c| {
                ExpressionLocation::try_new(
                    block_id.clone(),
                    c.id.clone(),
                    ExpressionKind::Standard,
                    c.expression.clone(),
                )
            })
            .collect()
    }

    pub(super) fn write_sites(&self) -> Vec<WriteSite> {
        if self.output.is_empty() {
            return Vec::new();
        }
        let contributing: HashSet<Arc<str>> =
            self.conditions.iter().map(|c| c.id.clone()).collect();
        vec![WriteSite {
            path: self.output.clone(),
            expression_id: None,
            resolved_type: VariableType::Bool,
            contributing_expr_ids: contributing,
        }]
    }

    pub(super) fn analyze(&self, cx: &mut AnalysisContext) {
        for condition in &self.conditions {
            cx.analyze_standard(&condition.expression, Some(condition.id.clone()));
        }
        if !self.output.is_empty() {
            cx.record_write(
                self.output.clone(),
                VariableType::Bool,
                None,
                Some(CursorTarget::AssertionOutput),
            );
        }
    }

    pub(super) fn write_target(&self, path: &str) -> Option<CursorTarget> {
        (!self.output.is_empty() && self.output.as_ref() == path)
            .then_some(CursorTarget::AssertionOutput)
    }

    pub(super) fn read_plan(&self, flatten: &mut ReadFlattenFn) -> BlockReadPlan {
        let mut unconditional = Vec::new();
        for condition in &self.conditions {
            unconditional.extend(flatten(&condition.expression, ExpressionKind::Standard));
        }
        BlockReadPlan {
            unconditional: BlockReadPlan::dedup(unconditional),
            conditional: ConditionalReads::None,
        }
    }

    pub(super) fn execute(&self, cx: &ExecutionContext) -> Result<BlockTrace, ExecutionError> {
        let mut isolate = cx.isolate.borrow_mut();
        let mut traces: Vec<ConditionTrace> = Vec::new();
        let result = self.evaluate_conditions(&mut isolate, cx, &mut traces)?;

        if !self.output.is_empty() {
            cx.write(&self.output, Variable::Bool(result));
        }

        Ok(BlockTrace::Assertion {
            result,
            conditions: traces,
        })
    }

    pub(super) fn nl(
        &self,
        policy_path: &Arc<str>,
        block_id: &Arc<str>,
        scope: &VariableType,
        is: &mut IntelliSense,
    ) -> Vec<NlExpression> {
        self.conditions
            .iter()
            .filter(|condition| !condition.expression.is_empty())
            .map(|condition| {
                NlExpression::project(
                    is,
                    policy_path,
                    block_id,
                    CursorTarget::Expression {
                        id: condition.id.clone(),
                    },
                    ExpressionKind::Standard,
                    condition.expression.as_ref(),
                    scope,
                )
            })
            .collect()
    }

    pub(super) fn resolve_cursor(
        &self,
        cursor: &Cursor,
        scope: VariableType,
    ) -> Option<(Arc<str>, ExpressionKind, VariableType)> {
        match &cursor.target {
            CursorTarget::AssertionOutput => {
                Some((self.output.clone(), ExpressionKind::Standard, scope))
            }
            CursorTarget::Expression { id } => {
                let cond = self.conditions.iter().find(|c| c.id == *id)?;
                Some((cond.expression.clone(), ExpressionKind::Unary, scope))
            }
            _ => None,
        }
    }

    fn evaluate_conditions(
        &self,
        isolate: &mut Isolate,
        cx: &ExecutionContext,
        traces: &mut Vec<ConditionTrace>,
    ) -> Result<bool, ExecutionError> {
        if self.conditions.is_empty() {
            return Ok(false);
        }

        let mut stack: Vec<Frame> = Vec::new();
        let mut acc = false;
        let mut started = false;
        let mut depth = 0u32;
        let mut combine_next = ConditionOperator::And;

        for condition in &self.conditions {
            while condition.depth > depth {
                stack.push(Frame {
                    acc,
                    started,
                    combine: combine_next,
                });
                acc = false;
                started = false;
                combine_next = ConditionOperator::And;
                depth += 1;
            }
            while condition.depth < depth {
                if let Some(frame) = stack.pop() {
                    acc = frame.close(acc);
                    started = true;
                }
                depth -= 1;
            }

            let cond_result = isolate
                .run_standard(&condition.expression)
                .map_err(|e| cx.expression_error(&condition.expression, e))?
                .as_bool()
                .unwrap_or(false);

            traces.push(ConditionTrace {
                id: condition.id.clone(),
                result: cond_result,
            });

            acc = if started {
                combine_next.apply(acc, cond_result)
            } else {
                cond_result
            };
            started = true;
            combine_next = condition.operator;
        }

        while let Some(frame) = stack.pop() {
            acc = frame.close(acc);
        }

        Ok(acc)
    }
}

struct Frame {
    acc: bool,
    started: bool,
    combine: ConditionOperator,
}

impl Frame {
    fn close(&self, group: bool) -> bool {
        if self.started {
            self.combine.apply(self.acc, group)
        } else {
            group
        }
    }
}