1#![forbid(unsafe_code)]
3#![allow(missing_docs, clippy::missing_errors_doc)]
4
5use std::{collections::BTreeMap, sync::Arc};
6
7pub use axioval_ir::NotEvaluatedReason;
8use axioval_ir::contract as schema;
9use axioval_ir::{Finding, NotEvaluated, ObjectId, Project, Report, RuleId};
10use thiserror::Error;
11
12#[derive(Debug, Error, PartialEq, Eq)]
14pub enum EngineError {
15 #[error(
17 "unsupported schema version `{version}` for {package_kind} `{package_id}`; supported: {supported}"
18 )]
19 UnsupportedSchemaVersion {
20 package_kind: &'static str,
21 package_id: String,
22 version: String,
23 supported: &'static str,
24 },
25 #[error("duplicate definition package `{0}`")]
27 DuplicateDefinitionPackage(String),
28 #[error("unknown capability `{0}`")]
30 UnknownCapability(String),
31 #[error("duplicate capability `{0}`")]
33 DuplicateCapability(String),
34 #[error("capability `{capability}` does not declare parameter `{parameter}`")]
36 UnknownParameter {
37 capability: String,
38 parameter: String,
39 },
40 #[error("capability `{capability}` requires parameter `{parameter}`")]
42 MissingParameter {
43 capability: String,
44 parameter: String,
45 },
46 #[error("capability `{capability}` parameter `{parameter}` has invalid type")]
48 InvalidParameterType {
49 capability: String,
50 parameter: String,
51 },
52 #[error("rule has duplicate parameter binding `{0}`")]
54 DuplicateBinding(String),
55 #[error("invalid rule id `{0}`")]
57 InvalidRuleId(String),
58 #[error("unknown rule definition `{0}`")]
60 UnknownDefinition(String),
61 #[error("missing definition package `{0}`")]
63 MissingDefinitionPackage(String),
64 #[error("definition `{definition}` conflicts with capability `{capability}`: {detail}")]
66 CapabilityContract {
67 definition: String,
68 capability: String,
69 detail: String,
70 },
71 #[error("duplicate rule id `{0}`")]
73 DuplicateRule(String),
74}
75
76#[derive(Clone, Copy, Debug, Eq, PartialEq)]
78pub enum ParameterType {
79 Boolean,
80 Integer,
81 Number,
82 String,
83 Quantity,
84 Enum,
85 Reference,
86 ObjectTypeReference,
87 PropertyReference,
88 Selector,
89 StringList,
90 ReferenceList,
91}
92impl ParameterType {
93 fn accepts(self, value: &schema::ParameterValue) -> bool {
94 matches!(
95 (self, value),
96 (Self::Boolean, schema::ParameterValue::Boolean { .. })
97 | (Self::Integer, schema::ParameterValue::Integer { .. })
98 | (Self::Number, schema::ParameterValue::Number { .. })
99 | (Self::String, schema::ParameterValue::String { .. })
100 | (Self::Quantity, schema::ParameterValue::Quantity { .. })
101 | (Self::Enum, schema::ParameterValue::Enum { .. })
102 | (Self::Reference, schema::ParameterValue::Reference { .. })
103 | (
104 Self::ObjectTypeReference,
105 schema::ParameterValue::ObjectTypeReference { .. }
106 )
107 | (
108 Self::PropertyReference,
109 schema::ParameterValue::PropertyReference { .. }
110 )
111 | (Self::Selector, schema::ParameterValue::Selector { .. })
112 | (Self::StringList, schema::ParameterValue::StringList { .. })
113 | (
114 Self::ReferenceList,
115 schema::ParameterValue::ReferenceList { .. }
116 )
117 )
118 }
119}
120#[derive(Clone, Debug, Eq, PartialEq)]
122pub struct ParameterDescriptor {
123 pub name: String,
124 pub parameter_type: ParameterType,
125 pub required: bool,
126}
127impl ParameterDescriptor {
128 pub fn required(name: impl Into<String>, parameter_type: ParameterType) -> Self {
130 Self {
131 name: name.into(),
132 parameter_type,
133 required: true,
134 }
135 }
136 pub fn optional(name: impl Into<String>, parameter_type: ParameterType) -> Self {
138 Self {
139 name: name.into(),
140 parameter_type,
141 required: false,
142 }
143 }
144}
145
146#[derive(Clone, Debug, PartialEq)]
148pub struct CompiledRule {
149 pub id: RuleId,
151 pub capability: String,
153 pub severity: schema::Severity,
155 pub selector: schema::Selector,
157 pub parameters: BTreeMap<String, schema::ParameterValue>,
159}
160
161pub struct RuleContext<'a> {
163 pub project: &'a Project,
165 pub services: &'a ServiceRegistry,
167}
168
169#[derive(Clone, Debug, Default, PartialEq)]
171pub struct CapabilityEvaluation {
172 findings: Vec<Finding>,
173 not_evaluated: Vec<CapabilityNotEvaluated>,
174}
175#[derive(Clone, Debug, PartialEq)]
177pub struct CapabilityNotEvaluated {
178 object_id: Option<ObjectId>,
179 reason: NotEvaluatedReason,
180 message: String,
181}
182impl CapabilityNotEvaluated {
183 #[must_use]
184 pub fn object_id(&self) -> Option<&ObjectId> {
185 self.object_id.as_ref()
186 }
187 #[must_use]
188 pub fn reason(&self) -> &NotEvaluatedReason {
189 &self.reason
190 }
191 #[must_use]
192 pub fn message(&self) -> &str {
193 &self.message
194 }
195}
196
197impl CapabilityEvaluation {
198 #[must_use]
200 pub fn findings(&self) -> &[Finding] {
201 &self.findings
202 }
203 #[must_use]
205 pub fn not_evaluated_outcomes(&self) -> &[CapabilityNotEvaluated] {
206 &self.not_evaluated
207 }
208 #[must_use]
210 pub fn evaluated(findings: Vec<Finding>) -> Self {
211 Self {
212 findings,
213 not_evaluated: Vec::new(),
214 }
215 }
216 #[must_use]
218 pub fn not_evaluated(reason: NotEvaluatedReason, message: impl Into<String>) -> Self {
219 let mut outcome = Self::default();
220 outcome.push_not_evaluated(reason, message);
221 outcome
222 }
223 pub fn push_finding(&mut self, finding: Finding) {
225 self.findings.push(finding);
226 }
227 pub fn push_not_evaluated(&mut self, reason: NotEvaluatedReason, message: impl Into<String>) {
229 self.push_unavailable(None, reason, message);
230 }
231 pub fn push_object_not_evaluated(
233 &mut self,
234 object_id: ObjectId,
235 reason: NotEvaluatedReason,
236 message: impl Into<String>,
237 ) {
238 self.push_unavailable(Some(object_id), reason, message);
239 }
240 fn push_unavailable(
241 &mut self,
242 object_id: Option<ObjectId>,
243 reason: NotEvaluatedReason,
244 message: impl Into<String>,
245 ) {
246 self.not_evaluated.push(CapabilityNotEvaluated {
247 object_id,
248 reason,
249 message: message.into(),
250 });
251 }
252}
253
254pub trait RuleCapability: Send + Sync {
256 fn id(&self) -> &'static str;
258 fn parameters(&self) -> Vec<ParameterDescriptor>;
260 fn evaluate(&self, context: &RuleContext<'_>, rule: &CompiledRule) -> CapabilityEvaluation;
262}
263
264#[derive(Clone, Default)]
266pub struct CapabilityRegistry {
267 capabilities: BTreeMap<String, Arc<dyn RuleCapability>>,
268}
269impl CapabilityRegistry {
270 pub fn new() -> Self {
272 Self::default()
273 }
274 pub fn register<C: RuleCapability + 'static>(
276 mut self,
277 capability: C,
278 ) -> Result<Self, EngineError> {
279 let id = capability.id().to_owned();
280 if self
281 .capabilities
282 .insert(id.clone(), Arc::new(capability))
283 .is_some()
284 {
285 return Err(EngineError::DuplicateCapability(id));
286 }
287 Ok(self)
288 }
289 pub fn get(&self, id: &str) -> Option<&Arc<dyn RuleCapability>> {
291 self.capabilities.get(id)
292 }
293}
294
295#[derive(Clone, Debug)]
297pub struct ExecutionPlan {
298 rules: Vec<CompiledRule>,
299}
300impl ExecutionPlan {
301 pub fn rules(&self) -> &[CompiledRule] {
303 &self.rules
304 }
305}
306
307mod compiler;
308mod free_space;
309mod metric_routing;
310mod properties;
311mod relationships;
312mod services;
313mod topology;
314mod walkability;
315pub use compiler::{SUPPORTED_SCHEMA_VERSION, compile};
316pub use free_space::{
317 AreaInterval, BoxClearance, ClearanceOutcome, ClearancePlacementEvidence, ClearanceRequest,
318 ClearanceShape, CompleteClearanceEvidence, CompletePlacementEvidence, CompleteSupportEvidence,
319 CylinderClearance, FrameOffsetPlacement, FreeAreaEvidence, FreeAreaRequest, FreeSpaceError,
320 FreeSpaceService, FreeSpaceServiceHandle, MetricDirection, MetricFrame, ObstructionEvidence,
321 PlacementDomain, PlacementOutcome, PlacementRequest, SignedDistanceInterval,
322 SupportedPlacement,
323};
324pub use metric_routing::{
325 BlockedMetricRouteEvidence, CompleteMetricEvidence, LengthInterval, MetricPoint,
326 MetricRouteEvidence, MetricRouteOutcome, MetricRouteRequest, MetricRoutingError,
327 MetricRoutingService, MetricRoutingServiceHandle, MobilityProfile, ThresholdVerdict,
328};
329pub use properties::{
330 CompletePropertyAbsenceEvidence, PropertyRequest, PropertyResolution, PropertyResolutionError,
331 PropertyResolutionService, PropertyResolutionServiceHandle, ResolvedProperty,
332};
333pub use relationships::{
334 CompleteRelationshipSelection, RelationshipQuery, RelationshipSelectionError,
335 RelationshipSelectionRequest, RelationshipSelectionService, RelationshipSelectionServiceHandle,
336 SemanticRelationship, TraversalDirection,
337};
338pub use services::{ServiceRegistry, ServiceRegistryError};
339pub use topology::{
340 CompleteTopologyEvidence, ConnectivityGraph, RouteOutcome, TopologyError, VerifiedConnection,
341};
342pub use walkability::{
343 VerifiedWalkablePassage, WalkabilityError, WalkabilityRegion, WalkabilityRegionId,
344 WalkabilityRequest, WalkabilityRouteOutcome, WalkabilityService, WalkabilityServiceHandle,
345 WalkabilitySnapshot,
346};
347
348pub struct Runtime {
350 registry: CapabilityRegistry,
351 services: ServiceRegistry,
352}
353impl Runtime {
354 pub fn new(registry: CapabilityRegistry) -> Self {
356 Self {
357 registry,
358 services: ServiceRegistry::new(),
359 }
360 }
361 #[must_use]
363 pub fn with_services(mut self, services: ServiceRegistry) -> Self {
364 self.services = services;
365 self
366 }
367 pub fn run(&self, project: &Project, plan: ExecutionPlan) -> Result<Report, EngineError> {
372 let context = RuleContext {
373 project,
374 services: &self.services,
375 };
376 let mut findings = Vec::new();
377 let mut not_evaluated = Vec::new();
378 for rule in plan.rules {
379 let capability = self
380 .registry
381 .get(&rule.capability)
382 .ok_or_else(|| EngineError::UnknownCapability(rule.capability.clone()))?;
383 let rule_id = rule.id.clone();
384 let evaluation = capability.evaluate(&context, &rule);
385 findings.extend(evaluation.findings);
386 not_evaluated.extend(evaluation.not_evaluated.into_iter().map(|outcome| {
387 NotEvaluated {
388 rule_id: rule_id.clone(),
389 object_id: outcome.object_id,
390 reason: outcome.reason,
391 message: outcome.message,
392 }
393 }));
394 }
395 findings.sort_by(|a, b| {
396 a.rule_id
397 .cmp(&b.rule_id)
398 .then_with(|| a.object_id.cmp(&b.object_id))
399 .then_with(|| a.message.cmp(&b.message))
400 });
401 not_evaluated.sort();
402 Ok(Report {
403 findings,
404 not_evaluated,
405 })
406 }
407}