1use 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
13pub const SUPPORTED_SCHEMA_VERSION: &str = "0.1.0";
15
16pub 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) = ¶meter.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 ¶meters {
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(¶meter.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}