axioval_engine/
compiler.rs

1//! Strict binding from portable schema packages to trusted executable plans.
2
3use std::collections::{BTreeMap, BTreeSet};
4
5use axioval_ir::contract::{ParameterKind, RuleFolder, RuleInstance};
6use axioval_ir::{DefinitionPackage, RuleId, RuleSetPackage};
7
8use crate::{
9    CapabilityRegistry, CompiledRule, EngineError, ExecutionPlan, ParameterDescriptor,
10    ParameterType,
11};
12
13/// Normalized Axioval Schema version implemented by this compiler.
14pub const SUPPORTED_SCHEMA_VERSION: &str = "0.1.0";
15
16/// Compiles a ruleset against its definition packages and host-controlled capabilities.
17pub fn compile(
18    registry: &CapabilityRegistry,
19    definitions: &[DefinitionPackage],
20    ruleset: &RuleSetPackage,
21) -> Result<ExecutionPlan, EngineError> {
22    validate_package_versions(definitions, ruleset)?;
23    let packages = collect_definition_packages(definitions)?;
24    for package_id in &ruleset.definition_packages {
25        if !packages.contains_key(package_id.as_str()) {
26            return Err(EngineError::MissingDefinitionPackage(package_id.clone()));
27        }
28    }
29    let mut catalog = BTreeMap::new();
30    for package_id in &ruleset.definition_packages {
31        for (id, definition) in &packages[package_id.as_str()].definitions {
32            if catalog.insert(id.as_str(), definition).is_some() {
33                return Err(EngineError::CapabilityContract {
34                    definition: id.clone(),
35                    capability: definition.capability.clone(),
36                    detail: "duplicate definition id".into(),
37                });
38            }
39        }
40    }
41    let mut authored = Vec::new();
42    flatten(&ruleset.root, &mut authored);
43    authored.sort_by(|left, right| left.id.cmp(&right.id));
44    let mut ids = BTreeSet::new();
45    let mut rules = Vec::new();
46    for rule in authored.into_iter().filter(|rule| rule.enabled) {
47        if !ids.insert(rule.id.as_str()) {
48            return Err(EngineError::DuplicateRule(rule.id.clone()));
49        }
50        let definition = catalog
51            .get(rule.definition_id.as_str())
52            .ok_or_else(|| EngineError::UnknownDefinition(rule.definition_id.clone()))?;
53        let capability = registry
54            .get(&definition.capability)
55            .ok_or_else(|| EngineError::UnknownCapability(definition.capability.clone()))?;
56        let descriptors = capability.parameters();
57        validate_signature(
58            &rule.definition_id,
59            &definition.capability,
60            &descriptors,
61            &definition.parameters,
62        )?;
63        let mut parameters = rule.parameters.clone();
64        for (name, parameter) in &definition.parameters {
65            if !parameters.contains_key(name) {
66                if let Some(default) = &parameter.default_value {
67                    parameters.insert(name.clone(), default.clone());
68                } else if parameter.required {
69                    return Err(EngineError::MissingParameter {
70                        capability: definition.capability.clone(),
71                        parameter: name.clone(),
72                    });
73                }
74            }
75        }
76        let known: BTreeMap<_, _> = descriptors
77            .iter()
78            .map(|item| (item.name.as_str(), item))
79            .collect();
80        for (name, value) in &parameters {
81            let descriptor =
82                known
83                    .get(name.as_str())
84                    .ok_or_else(|| EngineError::UnknownParameter {
85                        capability: definition.capability.clone(),
86                        parameter: name.clone(),
87                    })?;
88            if !descriptor.parameter_type.accepts(value) {
89                return Err(EngineError::InvalidParameterType {
90                    capability: definition.capability.clone(),
91                    parameter: name.clone(),
92                });
93            }
94            let definition_parameter = &definition.parameters[name];
95            if !definition_parameter.allowed_values.is_empty()
96                && !definition_parameter.allowed_values.contains(value)
97            {
98                return Err(EngineError::CapabilityContract {
99                    definition: rule.definition_id.clone(),
100                    capability: definition.capability.clone(),
101                    detail: format!("parameter `{name}` is outside allowedValues"),
102                });
103            }
104        }
105        rules.push(CompiledRule {
106            id: RuleId::new(rule.id.clone())
107                .map_err(|_| EngineError::InvalidRuleId(rule.id.clone()))?,
108            capability: definition.capability.clone(),
109            severity: rule.severity.clone(),
110            selector: rule.applicability.clone(),
111            parameters,
112        });
113    }
114    Ok(ExecutionPlan { rules })
115}
116
117fn collect_definition_packages(
118    definitions: &[DefinitionPackage],
119) -> Result<BTreeMap<&str, &DefinitionPackage>, EngineError> {
120    let mut packages = BTreeMap::new();
121    for package in definitions {
122        if packages
123            .insert(package.package.id.as_str(), package)
124            .is_some()
125        {
126            return Err(EngineError::DuplicateDefinitionPackage(
127                package.package.id.clone(),
128            ));
129        }
130    }
131    Ok(packages)
132}
133
134fn validate_package_versions(
135    definitions: &[DefinitionPackage],
136    ruleset: &RuleSetPackage,
137) -> Result<(), EngineError> {
138    validate_schema_version(
139        "ruleset package",
140        &ruleset.package.id,
141        &ruleset.schema_version,
142    )?;
143    for package in definitions {
144        validate_schema_version(
145            "definition package",
146            &package.package.id,
147            &package.schema_version,
148        )?;
149    }
150    Ok(())
151}
152
153fn validate_schema_version(
154    package_kind: &'static str,
155    package_id: &str,
156    version: &str,
157) -> Result<(), EngineError> {
158    if version == SUPPORTED_SCHEMA_VERSION {
159        return Ok(());
160    }
161    Err(EngineError::UnsupportedSchemaVersion {
162        package_kind,
163        package_id: package_id.into(),
164        version: version.into(),
165        supported: SUPPORTED_SCHEMA_VERSION,
166    })
167}
168
169fn flatten<'a>(folder: &'a RuleFolder, out: &mut Vec<&'a RuleInstance>) {
170    out.extend(&folder.rules);
171    for child in &folder.folders {
172        flatten(child, out);
173    }
174}
175
176fn validate_signature(
177    definition_id: &str,
178    capability_id: &str,
179    descriptors: &[ParameterDescriptor],
180    parameters: &BTreeMap<String, axioval_ir::contract::ParameterDefinition>,
181) -> Result<(), EngineError> {
182    if descriptors.len() != parameters.len() {
183        return contract_error(definition_id, capability_id, "parameter count differs");
184    }
185    for descriptor in descriptors {
186        let Some(parameter) = parameters.get(&descriptor.name) else {
187            return contract_error(definition_id, capability_id, "parameter name differs");
188        };
189        if descriptor.required != parameter.required
190            || descriptor.parameter_type != from_kind(&parameter.kind)
191        {
192            return contract_error(definition_id, capability_id, "parameter signature differs");
193        }
194    }
195    Ok(())
196}
197
198fn contract_error<T>(definition: &str, capability: &str, detail: &str) -> Result<T, EngineError> {
199    Err(EngineError::CapabilityContract {
200        definition: definition.into(),
201        capability: capability.into(),
202        detail: detail.into(),
203    })
204}
205
206fn from_kind(kind: &ParameterKind) -> ParameterType {
207    match kind {
208        ParameterKind::String => ParameterType::String,
209        ParameterKind::Boolean => ParameterType::Boolean,
210        ParameterKind::Integer => ParameterType::Integer,
211        ParameterKind::Number => ParameterType::Number,
212        ParameterKind::Quantity => ParameterType::Quantity,
213        ParameterKind::Enum => ParameterType::Enum,
214        ParameterKind::Reference => ParameterType::Reference,
215        ParameterKind::ObjectTypeReference => ParameterType::ObjectTypeReference,
216        ParameterKind::PropertyReference => ParameterType::PropertyReference,
217        ParameterKind::Selector => ParameterType::Selector,
218        ParameterKind::StringList => ParameterType::StringList,
219        ParameterKind::ReferenceList => ParameterType::ReferenceList,
220    }
221}