zen-engine 1.0.0-beta.11

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

use ahash::{AHasher, HashMap, HashMapExt, HashSet, HashSetExt};
use serde_json::{Map, Value};

use crate::loader::DynamicLoader;
use crate::policy::raw::BlockDoc;

pub(crate) fn schema_references_dictionary(value: &Value) -> bool {
    match value {
        Value::Object(map) => {
            map.contains_key("$dictionary") || map.values().any(schema_references_dictionary)
        }
        Value::Array(items) => items.iter().any(schema_references_dictionary),
        _ => false,
    }
}

pub(crate) async fn load_import_dictionaries(
    loader: &DynamicLoader,
    imports: &[Arc<str>],
) -> Result<HashMap<Arc<str>, Vec<Arc<str>>>, String> {
    let mut out: HashMap<Arc<str>, Vec<Arc<str>>> = HashMap::new();
    let mut visited: HashSet<Arc<str>> = HashSet::new();
    let mut queue: Vec<Arc<str>> = imports.to_vec();
    while let Some(key) = queue.pop() {
        if !visited.insert(key.clone()) {
            continue;
        }
        let loaded = loader
            .load(key.as_ref())
            .await
            .map_err(|error| format!("failed to load imported policy '{key}': {error:?}"))?;
        let Some(policy) = loaded.as_policy() else {
            continue;
        };
        let document = policy.0.clone();
        queue.extend(document.imports.iter().cloned());
        for block in &document.blocks {
            if let BlockDoc::Dictionary { data, .. } = block {
                out.entry(data.name.clone()).or_insert_with(|| {
                    data.entries
                        .iter()
                        .map(|entry| entry.value.clone())
                        .collect()
                });
            }
        }
    }
    Ok(out)
}

pub(crate) fn resolve_schema(
    schema: &Value,
    dictionaries: &HashMap<Arc<str>, Vec<Arc<str>>>,
) -> Result<(Value, u64), String> {
    let mut hasher = AHasher::default();
    let resolved = rewrite(schema, dictionaries, &mut hasher)?;
    Ok((resolved, hasher.finish()))
}

fn rewrite(
    value: &Value,
    dictionaries: &HashMap<Arc<str>, Vec<Arc<str>>>,
    hasher: &mut AHasher,
) -> Result<Value, String> {
    match value {
        Value::Object(map) => {
            if let Some(name) = map.get("$dictionary") {
                let Some(name) = name.as_str() else {
                    return Err("$dictionary must be a string dictionary name".to_string());
                };
                let Some(values) = dictionaries.get(name) else {
                    return Err(format!(
                        "unknown dictionary '{name}' in schema: no dictionary with that name is reachable through the graph's imports"
                    ));
                };
                hasher.write(name.as_bytes());
                let mut next = Map::new();
                for (key, entry) in map {
                    if key == "$dictionary" || key == "type" || key == "enum" {
                        continue;
                    }
                    next.insert(key.clone(), entry.clone());
                }
                next.insert("type".to_string(), Value::String("string".to_string()));
                next.insert(
                    "enum".to_string(),
                    Value::Array(
                        values
                            .iter()
                            .map(|entry| {
                                hasher.write(entry.as_bytes());
                                Value::String(entry.to_string())
                            })
                            .collect(),
                    ),
                );
                return Ok(Value::Object(next));
            }
            let mut next = Map::new();
            for (key, entry) in map {
                next.insert(key.clone(), rewrite(entry, dictionaries, hasher)?);
            }
            Ok(Value::Object(next))
        }
        Value::Array(items) => Ok(Value::Array(
            items
                .iter()
                .map(|entry| rewrite(entry, dictionaries, hasher))
                .collect::<Result<_, _>>()?,
        )),
        _ => Ok(value.clone()),
    }
}