use std::{
collections::{BTreeMap, BTreeSet},
sync::Arc,
};
use axioval_ir::contract::{
ClassificationDefinition, ColumnKind, GateCondition, ParameterKind, ParameterValue,
RuleApplicability, RuleDefinition, RuleFolder, RuleGate, RuleInstance, Selector,
TableColumnDefinition, TableRow,
};
use axioval_ir::{DefinitionPackage, RuleId, RuleSetPackage};
use crate::concepts::{ConceptCatalog, ConceptKind};
use crate::refinement::{RuleRefinement, validate_bands};
use crate::rule_outcomes;
use crate::{
CapabilityRegistry, CompiledRule, DeferredRule, EngineError, ExecutionPlan,
ParameterDescriptor, ParameterType, TableColumn,
};
pub const SUPPORTED_SCHEMA_VERSION: &str = "0.1.0";
pub fn compile(
registry: &CapabilityRegistry,
definitions: &[DefinitionPackage],
ruleset: &RuleSetPackage,
) -> Result<ExecutionPlan, EngineError> {
validate_package_versions(definitions, ruleset)?;
let packages = collect_definition_packages(definitions)?;
for package_id in &ruleset.definition_packages {
if !packages.contains_key(package_id.as_str()) {
return Err(EngineError::MissingDefinitionPackage(package_id.clone()));
}
}
let catalog = definition_catalog(ruleset, &packages)?;
let mut concepts = concept_catalog(ruleset, &packages)?;
concepts.declare_classifications(ruleset.classifications.keys().map(String::as_str));
let classifications = classifications(registry, &concepts, ruleset)?;
let mut authored = Vec::new();
flatten(&ruleset.root, &[], &mut authored);
authored.sort_by(|(left, _), (right, _)| left.id.cmp(&right.id));
let known: BTreeSet<&str> = authored.iter().map(|(rule, _)| rule.id.as_str()).collect();
let disabled: BTreeSet<&str> = authored
.iter()
.filter(|(rule, _)| !rule.enabled)
.map(|(rule, _)| rule.id.as_str())
.collect();
let mut ids = BTreeSet::new();
let mut rules = Vec::new();
let mut deferred = Vec::new();
let mut refinements = BTreeMap::new();
let mut gates = BTreeMap::new();
let mut dependencies: BTreeMap<RuleId, BTreeSet<RuleId>> = BTreeMap::new();
let mut recorded = BTreeSet::new();
let mut auxiliary = BTreeSet::new();
for (rule, folder_gates) in authored.into_iter().filter(|(rule, _)| rule.enabled) {
if !ids.insert(rule.id.as_str()) {
return Err(EngineError::DuplicateRule(rule.id.clone()));
}
let definition = catalog
.get(rule.definition_id.as_str())
.ok_or_else(|| EngineError::UnknownDefinition(rule.definition_id.clone()))?;
let parameters = bind_parameters(registry, rule, definition)?;
for value in parameters.values() {
validate_parameter_concepts(&concepts, &rule.id, value)?;
}
let id = RuleId::new(rule.id.clone())
.map_err(|_| EngineError::InvalidRuleId(rule.id.clone()))?;
let refinement = refinement(registry, &concepts, rule, &definition.capability)?;
let dependency = rule_dependencies(rule, &folder_gates, ¶meters, &refinement, &known)?;
recorded.extend(dependency.per_object.iter().cloned());
if rule.auxiliary {
auxiliary.insert(id.clone());
}
if !dependency.whole.is_empty() {
gates.insert(id.clone(), dependency.whole);
}
dependencies.insert(id.clone(), dependency.all);
if !refinement.is_empty() {
refinements.insert(id.clone(), refinement);
}
match applicability_selector(&concepts, rule)? {
Ok(selector) => rules.push(CompiledRule {
id,
capability: definition.capability.clone(),
severity: rule.severity.clone(),
selector: gated(selector, dependency.narrowing),
parameters,
}),
Err(groups) => deferred.push(DeferredRule {
id,
capability: definition.capability.clone(),
reason: format!(
"capability `{}` evaluates one population; applicability names {groups} target groups",
definition.capability,
),
}),
}
}
if let Some(unread) = auxiliary
.iter()
.find(|id| !dependencies.values().any(|parents| parents.contains(*id)))
{
return Err(EngineError::InvalidDependency {
rule: unread.to_string(),
detail: "the rule is auxiliary, but no enabled rule reads its outcome, so it \
would never be reported"
.into(),
});
}
let rules = defer_dependents(rules, &mut deferred, &dependencies, &disabled);
deferred.sort_by(|left, right| left.id.cmp(&right.id));
let rules = ordered(registry, rules, &dependencies, &recorded)?;
Ok(ExecutionPlan {
rules,
deferred,
concepts: Arc::new(concepts),
refinements,
gates,
recorded,
auxiliary,
classifications,
})
}
fn classifications(
registry: &CapabilityRegistry,
concepts: &ConceptCatalog,
ruleset: &RuleSetPackage,
) -> Result<Vec<ClassificationDefinition>, EngineError> {
let mut read_by: BTreeMap<&str, BTreeSet<&str>> = BTreeMap::new();
for (key, definition) in &ruleset.classifications {
let invalid = |detail: String| EngineError::InvalidClassification {
classification: key.clone(),
detail,
};
if *key != definition.id {
return Err(invalid(format!(
"is declared under the key `{key}`, not its id"
)));
}
if definition.id.trim().is_empty() {
return Err(invalid("its id is blank".into()));
}
if definition.rows.is_empty() {
return Err(invalid("it has no rows".into()));
}
if registry.refiner().is_none() {
return Err(invalid(
"the host registered no outcome refiner to evaluate its rows".into(),
));
}
let mut read = BTreeSet::new();
for (index, row) in definition.rows.iter().enumerate() {
if row.class.trim().is_empty() {
return Err(invalid(format!("row {index} assigns a blank class")));
}
let context = format!("{}#{index}", definition.id);
validate_selector_concepts(concepts, &context, &row.selector)?;
let mut rules = BTreeSet::new();
rule_outcomes::selector_references(&row.selector, &mut rules);
if !rules.is_empty() {
return Err(invalid(format!(
"row {index} reads a rule's outcome; classes are derived before any rule runs"
)));
}
classifications_read(&row.selector, &mut read);
}
read_by.insert(definition.id.as_str(), read);
}
let mut ordered: Vec<ClassificationDefinition> = Vec::new();
let mut pending: BTreeSet<&str> = read_by.keys().copied().collect();
while !pending.is_empty() {
let next = pending
.iter()
.copied()
.find(|id| read_by[id].iter().all(|needed| !pending.contains(needed)));
let Some(next) = next else {
let first = pending.iter().next().copied().unwrap_or_default();
return Err(EngineError::InvalidClassification {
classification: first.to_owned(),
detail: format!(
"the classifications {} read one another in a cycle",
pending
.iter()
.map(|id| format!("`{id}`"))
.collect::<Vec<_>>()
.join(", ")
),
});
};
pending.remove(next);
ordered.push(ruleset.classifications[next].clone());
}
Ok(ordered)
}
fn classifications_read<'a>(selector: &'a Selector, out: &mut BTreeSet<&'a str>) {
match selector {
Selector::Property {
property_set: Some(set),
property,
..
} if set == axioval_ir::CLASSIFICATION_SET => {
out.insert(property);
}
Selector::AllOf { operands } | Selector::AnyOf { operands } => {
for operand in operands {
classifications_read(operand, out);
}
}
Selector::Not { operand } => classifications_read(operand, out),
Selector::Related { selector, .. } => classifications_read(selector, out),
_ => {}
}
}
fn ordered(
registry: &CapabilityRegistry,
rules: Vec<CompiledRule>,
dependencies: &BTreeMap<RuleId, BTreeSet<RuleId>>,
recorded: &BTreeSet<RuleId>,
) -> Result<Vec<CompiledRule>, EngineError> {
let rules = rule_outcomes::dependency_order(rules, dependencies).map_err(|cycle| {
EngineError::InvalidDependency {
rule: cycle[0].to_string(),
detail: format!(
"the rules {} depend on one another's outcomes in a cycle",
cycle
.iter()
.map(|id| format!("`{id}`"))
.collect::<Vec<_>>()
.join(", ")
),
}
})?;
if let Some(reader) = rules.iter().find(|rule| {
dependencies[&rule.id]
.iter()
.any(|parent| recorded.contains(parent))
}) && registry.refiner().is_none()
{
return Err(EngineError::InvalidDependency {
rule: reader.id.to_string(),
detail: "the rule reads another rule's outcomes per object, and the host \
registered no outcome refiner to record that rule's selection"
.into(),
});
}
Ok(rules)
}
struct RuleDependency {
all: BTreeSet<RuleId>,
per_object: BTreeSet<RuleId>,
whole: Vec<(RuleId, GateCondition)>,
narrowing: Vec<Selector>,
}
fn rule_dependencies(
rule: &RuleInstance,
folder_gates: &[&RuleGate],
parameters: &BTreeMap<String, ParameterValue>,
refinement: &RuleRefinement,
known: &BTreeSet<&str>,
) -> Result<RuleDependency, EngineError> {
let invalid = |detail: String| EngineError::InvalidDependency {
rule: rule.id.clone(),
detail,
};
let mut per_object: BTreeSet<&str> = BTreeSet::new();
match &rule.applicability {
RuleApplicability::Selector(selector) => {
rule_outcomes::selector_references(selector, &mut per_object);
}
RuleApplicability::Groups(groups) => {
for group in groups.groups.values() {
rule_outcomes::selector_references(&group.selector, &mut per_object);
}
}
}
for value in parameters.values() {
rule_outcomes::value_references(value, &mut per_object);
}
for entry in &refinement.severity_overrides {
rule_outcomes::selector_references(&entry.selector, &mut per_object);
}
let mut whole = Vec::new();
let mut narrowing = Vec::new();
for gate in folder_gates.iter().copied().chain(&rule.gate) {
match rule_outcomes::gate_selector(&gate.rule, gate.condition) {
Some(selector) => {
per_object.insert(&gate.rule);
narrowing.push(selector);
}
None => whole.push((gate.rule.as_str(), gate.condition)),
}
}
let rule_id = |name: &str| {
if name == rule.id {
return Err(invalid(
"the rule depends on its own outcome; a gate on a folder must name a rule \
outside it"
.into(),
));
}
if !known.contains(name) {
return Err(invalid(format!(
"the rule depends on rule `{name}`, which the ruleset does not define"
)));
}
RuleId::new(name).map_err(|_| EngineError::InvalidRuleId(name.into()))
};
let per_object = per_object
.into_iter()
.map(rule_id)
.collect::<Result<BTreeSet<_>, _>>()?;
let whole = whole
.into_iter()
.map(|(name, condition)| Ok((rule_id(name)?, condition)))
.collect::<Result<Vec<_>, EngineError>>()?;
let mut all = per_object.clone();
all.extend(whole.iter().map(|(parent, _)| parent.clone()));
Ok(RuleDependency {
all,
per_object,
whole,
narrowing,
})
}
fn gated(selector: &Selector, narrowing: Vec<Selector>) -> Selector {
if narrowing.is_empty() {
return selector.clone();
}
let mut operands = narrowing;
operands.push(selector.clone());
Selector::AllOf { operands }
}
fn defer_dependents(
mut rules: Vec<CompiledRule>,
deferred: &mut Vec<DeferredRule>,
dependencies: &BTreeMap<RuleId, BTreeSet<RuleId>>,
disabled: &BTreeSet<&str>,
) -> Vec<CompiledRule> {
let blocking = |parent: &RuleId, deferred: &[DeferredRule]| {
disabled.contains(parent.to_string().as_str()) || deferred.iter().any(|d| d.id == *parent)
};
while let Some(index) = rules.iter().position(|rule| {
dependencies[&rule.id]
.iter()
.any(|parent| blocking(parent, deferred))
}) {
let rule = rules.remove(index);
let parents = dependencies[&rule.id]
.iter()
.filter(|parent| blocking(parent, deferred))
.map(|parent| format!("`{parent}`"))
.collect::<Vec<_>>()
.join(", ");
deferred.push(DeferredRule {
id: rule.id,
capability: rule.capability,
reason: format!(
"the rule depends on the outcome of rule {parents}, which is disabled or \
cannot run"
),
});
}
rules
}
fn refinement(
registry: &CapabilityRegistry,
concepts: &ConceptCatalog,
rule: &RuleInstance,
capability: &str,
) -> Result<RuleRefinement, EngineError> {
let invalid = |detail: String| EngineError::InvalidRefinement {
rule: rule.id.clone(),
detail,
};
if !rule.severity_bands.is_empty() {
validate_bands(&rule.severity_bands).map_err(invalid)?;
let grades = registry
.get(capability)
.is_some_and(|capability| capability.grades_deviation());
if !grades {
return Err(invalid(format!(
"capability `{capability}` reports no deviation to grade by `severityBands`"
)));
}
}
for entry in &rule.severity_overrides {
validate_selector_concepts(concepts, &rule.id, &entry.selector)?;
}
for level in &rule.categories {
require_property(
concepts,
&rule.id,
level.property_set.as_deref(),
&level.property,
)?;
}
let refinement = RuleRefinement {
severity_bands: rule.severity_bands.clone(),
severity_overrides: rule.severity_overrides.clone(),
categories: rule.categories.clone(),
};
if refinement.needs_refiner() && registry.refiner().is_none() {
return Err(invalid(
"the rule refines its outcomes by reading the model, and the host registered no \
outcome refiner"
.into(),
));
}
Ok(refinement)
}
pub const QUALIFIED_RULE_SEPARATOR: char = '/';
pub fn compile_rulesets(
registry: &CapabilityRegistry,
definitions: &[DefinitionPackage],
rulesets: &[RuleSetPackage],
) -> Result<ExecutionPlan, EngineError> {
let [first, rest @ ..] = rulesets else {
return Err(EngineError::NoRuleSet);
};
if rest.is_empty() {
return compile(registry, definitions, first);
}
let mut packages_seen = BTreeSet::new();
let mut declared: Vec<&String> = Vec::new();
let mut by_package: BTreeMap<&str, Vec<CompiledRule>> = BTreeMap::new();
let mut deferred = Vec::new();
let mut refinements = BTreeMap::new();
let mut gates = BTreeMap::new();
let mut recorded = BTreeSet::new();
let mut auxiliary = BTreeSet::new();
let mut classifications: Vec<ClassificationDefinition> = Vec::new();
for ruleset in rulesets {
let package = &ruleset.package.id;
if !packages_seen.insert(package.as_str()) {
return Err(EngineError::DuplicateRuleSet(package.clone()));
}
for id in &ruleset.definition_packages {
if !declared.contains(&id) {
declared.push(id);
}
}
let plan = compile(registry, definitions, ruleset)?;
let qualify = |id: &RuleId| {
let qualified = format!("{package}{QUALIFIED_RULE_SEPARATOR}{id}");
RuleId::new(qualified.clone()).map_err(|_| EngineError::InvalidRuleId(qualified))
};
let rename = |name: &str| format!("{package}{QUALIFIED_RULE_SEPARATOR}{name}");
for mut rule in plan.rules {
rule.id = qualify(&rule.id)?;
rule_outcomes::rename_selector(&mut rule.selector, &rename);
for value in rule.parameters.values_mut() {
rule_outcomes::rename_value(value, &rename);
}
by_package.entry(package).or_default().push(rule);
}
for (id, mut refinement) in plan.refinements {
for entry in &mut refinement.severity_overrides {
rule_outcomes::rename_selector(&mut entry.selector, &rename);
}
refinements.insert(qualify(&id)?, refinement);
}
for (id, parents) in plan.gates {
let parents = parents
.into_iter()
.map(|(parent, condition)| Ok((qualify(&parent)?, condition)))
.collect::<Result<Vec<_>, EngineError>>()?;
gates.insert(qualify(&id)?, parents);
}
for id in plan.recorded {
recorded.insert(qualify(&id)?);
}
for id in plan.auxiliary {
auxiliary.insert(qualify(&id)?);
}
for definition in plan.classifications {
match classifications
.iter()
.find(|known| known.id == definition.id)
{
Some(known) if *known == definition => {}
Some(_) => {
return Err(EngineError::InvalidClassification {
classification: definition.id,
detail: "two rulesets declare it with different rows".into(),
});
}
None => classifications.push(definition),
}
}
for mut rule in plan.deferred {
rule.id = qualify(&rule.id)?;
deferred.push(rule);
}
}
let rules: Vec<CompiledRule> = by_package.into_values().flatten().collect();
deferred.sort_by(|left, right| left.id.cmp(&right.id));
let packages = collect_definition_packages(definitions)?;
let mut concepts = concepts_of(declared.into_iter(), &packages)?;
concepts.declare_classifications(classifications.iter().map(|c| c.id.as_str()));
Ok(ExecutionPlan {
rules,
deferred,
concepts: Arc::new(concepts),
refinements,
gates,
recorded,
auxiliary,
classifications,
})
}
fn definition_catalog<'a>(
ruleset: &RuleSetPackage,
packages: &BTreeMap<&str, &'a DefinitionPackage>,
) -> Result<BTreeMap<&'a str, &'a RuleDefinition>, EngineError> {
let mut catalog = BTreeMap::new();
for package_id in &ruleset.definition_packages {
for (id, definition) in &packages[package_id.as_str()].definitions {
if catalog.insert(id.as_str(), definition).is_some() {
return Err(EngineError::CapabilityContract {
definition: id.clone(),
capability: definition.capability.clone(),
detail: "duplicate definition id".into(),
});
}
}
}
Ok(catalog)
}
fn bind_parameters(
registry: &CapabilityRegistry,
rule: &RuleInstance,
definition: &RuleDefinition,
) -> Result<BTreeMap<String, ParameterValue>, EngineError> {
let capability = registry
.get(&definition.capability)
.ok_or_else(|| EngineError::UnknownCapability(definition.capability.clone()))?;
let descriptors = capability.parameters();
validate_signature(
&rule.definition_id,
&definition.capability,
&descriptors,
&definition.parameters,
)?;
let mut parameters = rule.parameters.clone();
for (name, parameter) in &definition.parameters {
if !parameters.contains_key(name) {
if let Some(default) = ¶meter.default_value {
parameters.insert(name.clone(), default.clone());
} else if parameter.required {
return Err(EngineError::MissingParameter {
capability: definition.capability.clone(),
parameter: name.clone(),
});
}
}
}
let known: BTreeMap<_, _> = descriptors
.iter()
.map(|item| (item.name.as_str(), item))
.collect();
for (name, value) in ¶meters {
let descriptor = known
.get(name.as_str())
.ok_or_else(|| EngineError::UnknownParameter {
capability: definition.capability.clone(),
parameter: name.clone(),
})?;
if !descriptor.parameter_type.accepts(value) {
return Err(EngineError::InvalidParameterType {
capability: definition.capability.clone(),
parameter: name.clone(),
});
}
if let (ParameterType::Table(columns), ParameterValue::Table { value: rows }) =
(descriptor.parameter_type, value)
{
for (row, cells) in rows.iter().enumerate() {
validate_row(columns, cells).map_err(|detail| EngineError::InvalidTableRow {
capability: definition.capability.clone(),
parameter: name.clone(),
row,
detail,
})?;
}
}
let definition_parameter = &definition.parameters[name];
if !definition_parameter.allowed_values.is_empty()
&& !definition_parameter.allowed_values.contains(value)
{
return Err(EngineError::CapabilityContract {
definition: rule.definition_id.clone(),
capability: definition.capability.clone(),
detail: format!("parameter `{name}` is outside allowedValues"),
});
}
}
Ok(parameters)
}
fn applicability_selector<'r>(
concepts: &ConceptCatalog,
rule: &'r RuleInstance,
) -> Result<Result<&'r Selector, usize>, EngineError> {
match &rule.applicability {
RuleApplicability::Selector(selector) => {
validate_selector_concepts(concepts, &rule.id, selector)?;
Ok(Ok(selector))
}
RuleApplicability::Groups(groups) => {
for group in groups.groups.values() {
validate_selector_concepts(concepts, &rule.id, &group.selector)?;
}
Ok(
match groups.groups.values().collect::<Vec<_>>().as_slice() {
[only] => Ok(&only.selector),
_ => Err(groups.groups.len()),
},
)
}
}
}
fn concept_catalog(
ruleset: &RuleSetPackage,
packages: &BTreeMap<&str, &DefinitionPackage>,
) -> Result<ConceptCatalog, EngineError> {
concepts_of(ruleset.definition_packages.iter(), packages)
}
fn concepts_of<'a>(
package_ids: impl Iterator<Item = &'a String>,
packages: &BTreeMap<&str, &DefinitionPackage>,
) -> Result<ConceptCatalog, EngineError> {
let mut catalog = ConceptCatalog::default();
for package_id in package_ids {
let package = packages[package_id.as_str()];
let entries = package
.object_types
.values()
.map(|c| (ConceptKind::ObjectType, &c.id, &c.external_names))
.chain(
package
.properties
.values()
.map(|c| (ConceptKind::Property, &c.id, &c.external_names)),
)
.chain(
package
.property_sets
.values()
.map(|c| (ConceptKind::PropertySet, &c.id, &c.external_names)),
);
for (kind, id, names) in entries {
if catalog.insert(kind, id, names).is_err() {
return Err(EngineError::DuplicateConcept(id.clone()));
}
}
}
Ok(catalog)
}
fn require_concept(
concepts: &ConceptCatalog,
rule: &str,
kind: ConceptKind,
concept: &str,
) -> Result<(), EngineError> {
if concepts.contains(kind, concept) {
Ok(())
} else {
Err(EngineError::UnknownConcept {
rule: rule.into(),
kind: kind.to_string(),
concept: concept.into(),
})
}
}
fn require_set_concept(
concepts: &ConceptCatalog,
rule: &str,
set: &str,
) -> Result<(), EngineError> {
if axioval_ir::is_reserved_set(set) {
return Ok(());
}
require_concept(concepts, rule, ConceptKind::PropertySet, set)
}
fn require_property(
concepts: &ConceptCatalog,
rule: &str,
set: Option<&str>,
property: &str,
) -> Result<(), EngineError> {
if let Some(set) = set {
match concepts.derives(set, property) {
Some(true) => return Ok(()),
Some(false) => {
return Err(EngineError::UnknownConcept {
rule: rule.into(),
kind: set.into(),
concept: property.into(),
});
}
None => require_set_concept(concepts, rule, set)?,
}
}
require_concept(concepts, rule, ConceptKind::Property, property)
}
fn validate_selector_concepts(
concepts: &ConceptCatalog,
rule: &str,
selector: &Selector,
) -> Result<(), EngineError> {
match selector {
Selector::All
| Selector::Classification { .. }
| Selector::Discipline { .. }
| Selector::RuleOutcome { .. } => Ok(()),
Selector::EntityType { object_type, .. } => {
require_concept(concepts, rule, ConceptKind::ObjectType, object_type)
}
Selector::Property {
property_set,
property,
value,
..
} => {
require_property(concepts, rule, property_set.as_deref(), property)?;
value
.iter()
.try_for_each(|value| validate_parameter_concepts(concepts, rule, value))
}
Selector::PropertyPattern { value, .. } | Selector::Source { value, .. } => value
.iter()
.try_for_each(|value| validate_parameter_concepts(concepts, rule, value)),
Selector::AllOf { operands } | Selector::AnyOf { operands } => operands
.iter()
.try_for_each(|operand| validate_selector_concepts(concepts, rule, operand)),
Selector::Not { operand } => validate_selector_concepts(concepts, rule, operand),
Selector::Related { selector, .. } => validate_selector_concepts(concepts, rule, selector),
}
}
fn validate_parameter_concepts(
concepts: &ConceptCatalog,
rule: &str,
value: &ParameterValue,
) -> Result<(), EngineError> {
match value {
ParameterValue::ObjectTypeReference { object_type, .. } => {
require_concept(concepts, rule, ConceptKind::ObjectType, object_type)
}
ParameterValue::PropertyReference {
property,
property_set,
} => require_property(concepts, rule, property_set.as_deref(), property),
ParameterValue::Selector { value } => validate_selector_concepts(concepts, rule, value),
ParameterValue::Table { value: rows } => rows
.iter()
.flat_map(TableRow::values)
.try_for_each(|cell| validate_parameter_concepts(concepts, rule, cell)),
_ => Ok(()),
}
}
fn collect_definition_packages(
definitions: &[DefinitionPackage],
) -> Result<BTreeMap<&str, &DefinitionPackage>, EngineError> {
let mut packages = BTreeMap::new();
for package in definitions {
if packages
.insert(package.package.id.as_str(), package)
.is_some()
{
return Err(EngineError::DuplicateDefinitionPackage(
package.package.id.clone(),
));
}
}
Ok(packages)
}
fn validate_package_versions(
definitions: &[DefinitionPackage],
ruleset: &RuleSetPackage,
) -> Result<(), EngineError> {
validate_schema_version(
"ruleset package",
&ruleset.package.id,
&ruleset.schema_version,
)?;
for package in definitions {
validate_schema_version(
"definition package",
&package.package.id,
&package.schema_version,
)?;
}
Ok(())
}
fn validate_schema_version(
package_kind: &'static str,
package_id: &str,
version: &str,
) -> Result<(), EngineError> {
if version == SUPPORTED_SCHEMA_VERSION {
return Ok(());
}
Err(EngineError::UnsupportedSchemaVersion {
package_kind,
package_id: package_id.into(),
version: version.into(),
supported: SUPPORTED_SCHEMA_VERSION,
})
}
fn flatten<'a>(
folder: &'a RuleFolder,
outer: &[&'a RuleGate],
out: &mut Vec<(&'a RuleInstance, Vec<&'a RuleGate>)>,
) {
let mut gates = outer.to_vec();
gates.extend(&folder.gate);
out.extend(folder.rules.iter().map(|rule| (rule, gates.clone())));
for child in &folder.folders {
flatten(child, &gates, out);
}
}
fn validate_signature(
definition_id: &str,
capability_id: &str,
descriptors: &[ParameterDescriptor],
parameters: &BTreeMap<String, axioval_ir::contract::ParameterDefinition>,
) -> Result<(), EngineError> {
if descriptors.len() != parameters.len() {
return contract_error(definition_id, capability_id, "parameter count differs");
}
for descriptor in descriptors {
let Some(parameter) = parameters.get(&descriptor.name) else {
return contract_error(definition_id, capability_id, "parameter name differs");
};
if descriptor.required != parameter.required
|| !same_type(descriptor.parameter_type, ¶meter.kind)
{
return contract_error(definition_id, capability_id, "parameter signature differs");
}
match descriptor.parameter_type {
ParameterType::Table(columns) => {
if !same_columns(columns, ¶meter.columns) {
return contract_error(
definition_id,
capability_id,
&format!("table parameter `{}` columns differ", descriptor.name),
);
}
if !parameter.allowed_values.is_empty() {
return contract_error(
definition_id,
capability_id,
&format!(
"table parameter `{}` must not declare allowedValues",
descriptor.name
),
);
}
}
_ if !parameter.columns.is_empty() => {
return contract_error(
definition_id,
capability_id,
&format!(
"only a table parameter declares columns, not `{}`",
descriptor.name
),
);
}
_ => {}
}
}
Ok(())
}
fn same_columns(trusted: &[TableColumn], declared: &[TableColumnDefinition]) -> bool {
let mut trusted: Vec<_> = trusted
.iter()
.map(|column| (column.id, column.kind, column.required))
.collect();
let mut declared: Vec<_> = declared
.iter()
.map(|column| (column.id.as_str(), column.kind, column.required))
.collect();
trusted.sort_unstable();
declared.sort_unstable();
let distinct = declared.windows(2).all(|pair| pair[0].0 != pair[1].0);
distinct && trusted == declared
}
fn validate_row(columns: &[TableColumn], row: &TableRow) -> Result<(), String> {
for (id, cell) in row {
let column = columns
.iter()
.find(|column| column.id == id)
.ok_or_else(|| format!("unknown column `{id}`"))?;
if !cell_fits(column.kind, cell) {
return Err(format!(
"column `{id}` takes a {} cell",
column.kind.as_str()
));
}
}
match columns
.iter()
.find(|column| column.required && !row.contains_key(column.id))
{
Some(column) => Err(format!("required column `{}` is empty", column.id)),
None => Ok(()),
}
}
fn cell_fits(kind: ColumnKind, cell: &ParameterValue) -> bool {
match (kind, cell) {
(ColumnKind::String, ParameterValue::String { .. })
| (ColumnKind::Integer, ParameterValue::Integer { .. })
| (ColumnKind::Boolean, ParameterValue::Boolean { .. })
| (ColumnKind::Selector, ParameterValue::Selector { .. })
| (ColumnKind::Reference, ParameterValue::Reference { .. })
| (ColumnKind::Date, ParameterValue::Date { .. })
| (ColumnKind::DateTime, ParameterValue::DateTime { .. }) => true,
(ColumnKind::TextPattern, ParameterValue::String { value }) => well_formed_pattern(value),
(ColumnKind::Number, ParameterValue::Number { value }) => value.is_finite(),
(ColumnKind::Quantity, ParameterValue::Quantity { value, unit }) => {
value.is_finite() && !unit.is_empty()
}
_ => false,
}
}
fn well_formed_pattern(pattern: &str) -> bool {
let mut chars = pattern.chars();
while let Some(c) = chars.next() {
if c == '\\' && chars.next().is_none() {
return false;
}
}
true
}
fn same_type(parameter_type: ParameterType, kind: &ParameterKind) -> bool {
match (parameter_type, kind) {
(ParameterType::Table(_), ParameterKind::Table) => true,
(ParameterType::Table(_), _) | (_, ParameterKind::Table) => false,
(parameter_type, kind) => parameter_type == from_kind(kind),
}
}
fn contract_error<T>(definition: &str, capability: &str, detail: &str) -> Result<T, EngineError> {
Err(EngineError::CapabilityContract {
definition: definition.into(),
capability: capability.into(),
detail: detail.into(),
})
}
fn from_kind(kind: &ParameterKind) -> ParameterType {
match kind {
ParameterKind::String => ParameterType::String,
ParameterKind::Boolean => ParameterType::Boolean,
ParameterKind::Integer => ParameterType::Integer,
ParameterKind::Number => ParameterType::Number,
ParameterKind::Quantity => ParameterType::Quantity,
ParameterKind::Enum => ParameterType::Enum,
ParameterKind::Date => ParameterType::Date,
ParameterKind::DateTime => ParameterType::DateTime,
ParameterKind::Reference => ParameterType::Reference,
ParameterKind::ObjectTypeReference => ParameterType::ObjectTypeReference,
ParameterKind::PropertyReference => ParameterType::PropertyReference,
ParameterKind::Selector => ParameterType::Selector,
ParameterKind::StringList => ParameterType::StringList,
ParameterKind::ReferenceList => ParameterType::ReferenceList,
ParameterKind::Table => ParameterType::Table(&[]),
}
}