zen-engine 2.0.1

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

use ahash::{HashMap, HashSet, HashSetExt};

use crate::policy::blocks::{IntelliSenseSource, ReadFlattener};
use crate::policy::queries::scope::{EntityForm, VariableTypeScope};
use crate::workspace::db::{Db, Snapshot};
use crate::workspace::types::{BlockRef, DependencyNode};
use zen_expression::variable::VariableType;

impl Db {
    pub fn dependencies(&self, target: &str) -> DependencyNode {
        let snapshot = self.snapshot();
        let unit = self.unit_for_property(target);
        let entity_form = EntityForm::new(&unit.entity_sources);
        let enriched = self.enriched_of_unit(&unit);
        let scope = &enriched.scope;
        let mut visited: HashSet<Arc<str>> = HashSet::new();
        let mut expr_ids_cache: HashMap<BlockRef, HashMap<Arc<str>, HashSet<Arc<str>>>> =
            HashMap::default();
        let mut node_cache: HashMap<Arc<str>, DependencyNode> = HashMap::default();

        if unit.dep_graph.writer_for(target).is_none() {
            if let Some(node) = self.field_dependency_node(
                &unit.dep_graph,
                &snapshot,
                &entity_form,
                scope,
                target,
                &mut visited,
                &mut expr_ids_cache,
                &mut node_cache,
            ) {
                return node;
            }
        }

        let (node, _tainted) = Self::build_dep_node(
            &unit.dep_graph,
            &snapshot.shallow,
            &snapshot.rule_by_ref,
            &entity_form,
            scope,
            target,
            false,
            &mut visited,
            &mut expr_ids_cache,
            &mut node_cache,
        );
        node
    }

    #[allow(clippy::too_many_arguments)]
    fn field_dependency_node(
        &self,
        graph: &crate::policy::queries::dependency::DependencyGraph,
        snapshot: &Snapshot,
        entity_form: &EntityForm,
        scope: &VariableType,
        target: &str,
        visited: &mut HashSet<Arc<str>>,
        expr_ids_cache: &mut HashMap<BlockRef, HashMap<Arc<str>, HashSet<Arc<str>>>>,
        node_cache: &mut HashMap<Arc<str>, DependencyNode>,
    ) -> Option<DependencyNode> {
        let target_type = scope.resolve_at(target).to_acyclic();
        let segments: Vec<&str> = target.split('.').collect();
        if segments.len() < 2 {
            return None;
        }
        let (prefix, owner, tail_start) = (1..segments.len()).rev().find_map(|i| {
            let prefix = segments[..i].join(".");
            graph
                .writer_for(&prefix)
                .cloned()
                .map(|owner| (prefix, owner, i))
        })?;
        let tail: Vec<&str> = segments[tail_start..].to_vec();

        let Some(block) = snapshot.rule_by_ref.get(&owner) else {
            return None;
        };

        let undecomposable = DependencyNode {
            property: Arc::from(target),
            written_by: Some(owner.clone()),
            unresolved: true,
            resolved_type: target_type.clone(),
            deps: Vec::new(),
        };

        let value_exprs = block.kind.write_value_expressions(&prefix);
        if value_exprs.is_empty() {
            return Some(undecomposable);
        }

        let mut flat: Vec<crate::policy::blocks::PropertyRead> = Vec::new();
        let mut navigated = false;
        {
            let is = self.intellisense();
            let mut is = is.borrow_mut();
            for expr in &value_exprs {
                if let Some(reads) = IntelliSenseSource::field_reads(&mut is, expr, &tail) {
                    navigated = true;
                    ReadFlattener::extend_from_deps(&reads, &None, &mut flat);
                }
            }
        }
        if !navigated {
            return Some(undecomposable);
        }

        let mut seen: HashSet<Arc<str>> = HashSet::new();
        let mut deps: Vec<DependencyNode> = Vec::new();
        for read in flat {
            if read.path.as_ref() == "$" || !seen.insert(read.path.clone()) {
                continue;
            }
            let (child, _tainted) = Self::build_dep_node(
                graph,
                &snapshot.shallow,
                &snapshot.rule_by_ref,
                entity_form,
                scope,
                &read.path,
                read.unresolved,
                visited,
                expr_ids_cache,
                node_cache,
            );
            deps.push(child);
        }
        deps.sort_by(|a, b| a.property.cmp(&b.property));

        Some(DependencyNode {
            property: Arc::from(target),
            written_by: Some(owner),
            unresolved: false,
            resolved_type: target_type,
            deps,
        })
    }

    #[allow(clippy::too_many_arguments)]
    fn build_dep_node(
        graph: &crate::policy::queries::dependency::DependencyGraph,
        shallow: &crate::policy::queries::dependency::ShallowAnalyses,
        rule_by_ref: &HashMap<BlockRef, Arc<crate::policy::blocks::Block>>,
        entity_form: &crate::policy::queries::scope::EntityForm,
        scope: &VariableType,
        target: &str,
        target_unresolved: bool,
        visited: &mut HashSet<Arc<str>>,
        expr_ids_cache: &mut HashMap<BlockRef, HashMap<Arc<str>, HashSet<Arc<str>>>>,
        node_cache: &mut HashMap<Arc<str>, DependencyNode>,
    ) -> (DependencyNode, bool) {
        let property: Arc<str> = if graph.writer_for(target).is_some() {
            Arc::from(target)
        } else {
            match entity_form.rewrite(target) {
                Some(ef) if graph.writer_for(&ef).is_some() => Arc::from(ef),
                _ => Arc::from(target),
            }
        };
        if let Some(cached) = node_cache.get(&property) {
            return (cached.clone(), false);
        }
        let written_by = graph.writer_for(&property).cloned();

        let unresolved = written_by.is_none() && target_unresolved;

        let resolved_type = graph
            .node_map
            .get(property.as_ref())
            .map(|&idx| graph.graph[idx].resolved_type_in(scope, &property))
            .unwrap_or_else(|| scope.resolve_at(&property).to_acyclic());

        if !visited.insert(property.clone()) {
            return (
                DependencyNode {
                    property,
                    written_by,
                    unresolved,
                    resolved_type,
                    deps: Vec::new(),
                },
                true,
            );
        }
        let Some(owner) = &written_by else {
            visited.remove(&property);
            let node = DependencyNode {
                property: property.clone(),
                written_by: None,
                unresolved,
                resolved_type,
                deps: Vec::new(),
            };
            node_cache.insert(property, node.clone());
            return (node, false);
        };

        let block_expr_ids = expr_ids_cache.entry(owner.clone()).or_insert_with(|| {
            rule_by_ref
                .get(owner)
                .map(|block| block.kind.write_dependency_expr_ids())
                .unwrap_or_default()
        });
        let unfiltered = block_expr_ids.is_empty();
        let allowed: HashSet<Arc<str>> = match block_expr_ids.get(&property) {
            Some(ids) => ids.clone(),
            None => block_expr_ids
                .iter()
                .filter(|(k, _)| {
                    crate::policy::queries::dependency::PathPrefix::extends(&property, k)
                })
                .flat_map(|(_, ids)| ids.iter().cloned())
                .collect(),
        };

        let direct_paths: Vec<(Arc<str>, bool)> = shallow
            .for_block(owner)
            .map(|r| {
                let mut by_path: HashMap<Arc<str>, bool> = HashMap::default();
                for read in r.reads.iter().filter(|read| match &read.expression_id {
                    _ if unfiltered => true,
                    Some(id) => allowed.contains(id),
                    None => true,
                }) {
                    by_path
                        .entry(read.path.clone())
                        .and_modify(|u| *u = *u && read.unresolved)
                        .or_insert(read.unresolved);
                }
                let mut out: Vec<(Arc<str>, bool)> = by_path.into_iter().collect();
                out.sort_by(|a, b| a.0.cmp(&b.0));
                out
            })
            .unwrap_or_default();

        let mut tainted = false;
        let deps: Vec<DependencyNode> = direct_paths
            .into_iter()
            .map(|(path, child_unresolved)| {
                let (child, child_tainted) = Self::build_dep_node(
                    graph,
                    shallow,
                    rule_by_ref,
                    entity_form,
                    scope,
                    &path,
                    child_unresolved,
                    visited,
                    expr_ids_cache,
                    node_cache,
                );
                tainted |= child_tainted;
                child
            })
            .collect();

        visited.remove(&property);
        let node = DependencyNode {
            property: property.clone(),
            written_by,
            unresolved,
            resolved_type,
            deps,
        };
        if !tainted {
            node_cache.insert(property, node.clone());
        }
        (node, tainted)
    }
}