#![forbid(unsafe_code)]
#![allow(missing_docs, clippy::missing_errors_doc)]
use std::{collections::BTreeMap, sync::Arc};
pub use axioval_ir::NotEvaluatedReason;
use axioval_ir::contract as schema;
use axioval_ir::{Finding, NotEvaluated, ObjectId, Project, Report, RuleId};
use thiserror::Error;
mod session;
#[derive(Debug, Error, PartialEq, Eq)]
pub enum EngineError {
#[error(
"unsupported schema version `{version}` for {package_kind} `{package_id}`; supported: {supported}"
)]
UnsupportedSchemaVersion {
package_kind: &'static str,
package_id: String,
version: String,
supported: &'static str,
},
#[error("duplicate definition package `{0}`")]
DuplicateDefinitionPackage(String),
#[error("unknown capability `{0}`")]
UnknownCapability(String),
#[error("duplicate capability `{0}`")]
DuplicateCapability(String),
#[error("capability `{capability}` does not declare parameter `{parameter}`")]
UnknownParameter {
capability: String,
parameter: String,
},
#[error("capability `{capability}` requires parameter `{parameter}`")]
MissingParameter {
capability: String,
parameter: String,
},
#[error("capability `{capability}` parameter `{parameter}` has invalid type")]
InvalidParameterType {
capability: String,
parameter: String,
},
#[error("rule has duplicate parameter binding `{0}`")]
DuplicateBinding(String),
#[error("invalid rule id `{0}`")]
InvalidRuleId(String),
#[error("unknown rule definition `{0}`")]
UnknownDefinition(String),
#[error("missing definition package `{0}`")]
MissingDefinitionPackage(String),
#[error("definition `{definition}` conflicts with capability `{capability}`: {detail}")]
CapabilityContract {
definition: String,
capability: String,
detail: String,
},
#[error("duplicate rule id `{0}`")]
DuplicateRule(String),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ParameterType {
Boolean,
Integer,
Number,
String,
Quantity,
Enum,
Reference,
ObjectTypeReference,
PropertyReference,
Selector,
StringList,
ReferenceList,
}
impl ParameterType {
fn accepts(self, value: &schema::ParameterValue) -> bool {
matches!(
(self, value),
(Self::Boolean, schema::ParameterValue::Boolean { .. })
| (Self::Integer, schema::ParameterValue::Integer { .. })
| (Self::Number, schema::ParameterValue::Number { .. })
| (Self::String, schema::ParameterValue::String { .. })
| (Self::Quantity, schema::ParameterValue::Quantity { .. })
| (Self::Enum, schema::ParameterValue::Enum { .. })
| (Self::Reference, schema::ParameterValue::Reference { .. })
| (
Self::ObjectTypeReference,
schema::ParameterValue::ObjectTypeReference { .. }
)
| (
Self::PropertyReference,
schema::ParameterValue::PropertyReference { .. }
)
| (Self::Selector, schema::ParameterValue::Selector { .. })
| (Self::StringList, schema::ParameterValue::StringList { .. })
| (
Self::ReferenceList,
schema::ParameterValue::ReferenceList { .. }
)
)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ParameterDescriptor {
pub name: String,
pub parameter_type: ParameterType,
pub required: bool,
}
impl ParameterDescriptor {
pub fn required(name: impl Into<String>, parameter_type: ParameterType) -> Self {
Self {
name: name.into(),
parameter_type,
required: true,
}
}
pub fn optional(name: impl Into<String>, parameter_type: ParameterType) -> Self {
Self {
name: name.into(),
parameter_type,
required: false,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct CompiledRule {
pub id: RuleId,
pub capability: String,
pub severity: schema::Severity,
pub selector: schema::Selector,
pub parameters: BTreeMap<String, schema::ParameterValue>,
}
pub struct RuleContext<'a> {
pub project: &'a Project,
pub services: &'a ServiceRegistry,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct CapabilityEvaluation {
findings: Vec<Finding>,
not_evaluated: Vec<CapabilityNotEvaluated>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct CapabilityNotEvaluated {
object_id: Option<ObjectId>,
reason: NotEvaluatedReason,
message: String,
}
impl CapabilityNotEvaluated {
#[must_use]
pub fn object_id(&self) -> Option<&ObjectId> {
self.object_id.as_ref()
}
#[must_use]
pub fn reason(&self) -> &NotEvaluatedReason {
&self.reason
}
#[must_use]
pub fn message(&self) -> &str {
&self.message
}
}
impl CapabilityEvaluation {
#[must_use]
pub fn findings(&self) -> &[Finding] {
&self.findings
}
#[must_use]
pub fn not_evaluated_outcomes(&self) -> &[CapabilityNotEvaluated] {
&self.not_evaluated
}
#[must_use]
pub fn evaluated(findings: Vec<Finding>) -> Self {
Self {
findings,
not_evaluated: Vec::new(),
}
}
#[must_use]
pub fn not_evaluated(reason: NotEvaluatedReason, message: impl Into<String>) -> Self {
let mut outcome = Self::default();
outcome.push_not_evaluated(reason, message);
outcome
}
pub fn push_finding(&mut self, finding: Finding) {
self.findings.push(finding);
}
pub fn push_not_evaluated(&mut self, reason: NotEvaluatedReason, message: impl Into<String>) {
self.push_unavailable(None, reason, message);
}
pub fn push_object_not_evaluated(
&mut self,
object_id: ObjectId,
reason: NotEvaluatedReason,
message: impl Into<String>,
) {
self.push_unavailable(Some(object_id), reason, message);
}
fn push_unavailable(
&mut self,
object_id: Option<ObjectId>,
reason: NotEvaluatedReason,
message: impl Into<String>,
) {
self.not_evaluated.push(CapabilityNotEvaluated {
object_id,
reason,
message: message.into(),
});
}
}
pub trait RuleCapability: Send + Sync {
fn id(&self) -> &'static str;
fn parameters(&self) -> Vec<ParameterDescriptor>;
fn evaluate(&self, context: &RuleContext<'_>, rule: &CompiledRule) -> CapabilityEvaluation;
}
#[derive(Clone, Default)]
pub struct CapabilityRegistry {
capabilities: BTreeMap<String, Arc<dyn RuleCapability>>,
}
impl CapabilityRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register<C: RuleCapability + 'static>(
mut self,
capability: C,
) -> Result<Self, EngineError> {
let id = capability.id().to_owned();
if self
.capabilities
.insert(id.clone(), Arc::new(capability))
.is_some()
{
return Err(EngineError::DuplicateCapability(id));
}
Ok(self)
}
pub fn get(&self, id: &str) -> Option<&Arc<dyn RuleCapability>> {
self.capabilities.get(id)
}
}
#[derive(Clone, Debug)]
pub struct ExecutionPlan {
rules: Vec<CompiledRule>,
}
impl ExecutionPlan {
pub fn rules(&self) -> &[CompiledRule] {
&self.rules
}
}
mod compiler;
mod free_space;
mod linear_quantity;
mod metric_routing;
mod properties;
mod relationships;
mod services;
mod topology;
mod walkability;
pub use compiler::{SUPPORTED_SCHEMA_VERSION, compile};
pub use free_space::{
AreaInterval, BoxClearance, ClearanceOutcome, ClearancePlacementEvidence, ClearanceRequest,
ClearanceShape, CompleteClearanceEvidence, CompletePlacementEvidence, CompleteSupportEvidence,
CylinderClearance, FrameOffsetPlacement, FreeAreaEvidence, FreeAreaRequest, FreeSpaceError,
FreeSpaceService, FreeSpaceServiceHandle, MetricDirection, MetricFrame, ObstructionEvidence,
PlacementDomain, PlacementOutcome, PlacementRequest, SignedDistanceInterval,
SupportedPlacement,
};
pub use linear_quantity::{
LinearInterval, LinearQuantityError, LinearQuantityEvidence, LinearQuantityKind,
LinearQuantityRequest, LinearQuantityService, LinearQuantityServiceHandle, ShelfGeometry,
};
pub use metric_routing::{
BlockedMetricRouteEvidence, CompleteMetricEvidence, LengthInterval, MetricPoint,
MetricRouteEvidence, MetricRouteOutcome, MetricRouteRequest, MetricRoutingError,
MetricRoutingService, MetricRoutingServiceHandle, MobilityProfile, ThresholdVerdict,
};
pub use properties::{
CompletePropertyAbsenceEvidence, PropertyRequest, PropertyResolution, PropertyResolutionError,
PropertyResolutionService, PropertyResolutionServiceHandle, ResolvedProperty,
};
pub use relationships::{
CompleteRelationshipSelection, RelationshipQuery, RelationshipSelectionError,
RelationshipSelectionRequest, RelationshipSelectionService, RelationshipSelectionServiceHandle,
SemanticRelationship, TraversalDirection,
};
pub use services::{ServiceRegistry, ServiceRegistryError};
pub use session::{EvidenceSession, EvidenceSessionError, SnapshotBoundService, SourceSnapshot};
pub use topology::{
CompleteTopologyEvidence, ConnectivityGraph, RouteOutcome, TopologyError, VerifiedConnection,
};
pub use walkability::{
VerifiedWalkablePassage, WalkabilityError, WalkabilityRegion, WalkabilityRegionId,
WalkabilityRequest, WalkabilityRouteOutcome, WalkabilityService, WalkabilityServiceHandle,
WalkabilitySnapshot,
};
pub struct Runtime {
registry: CapabilityRegistry,
services: ServiceRegistry,
}
impl Runtime {
pub fn new(registry: CapabilityRegistry) -> Self {
Self {
registry,
services: ServiceRegistry::new(),
}
}
#[must_use]
pub fn with_services(mut self, services: ServiceRegistry) -> Self {
self.services = services;
self
}
pub fn run(&self, project: &Project, plan: ExecutionPlan) -> Result<Report, EngineError> {
self.run_with_services(project, &self.services, plan)
}
pub fn run_session(
&self,
session: &EvidenceSession,
plan: ExecutionPlan,
) -> Result<Report, EngineError> {
self.run_with_services(session.project(), session.services(), plan)
}
fn run_with_services(
&self,
project: &Project,
services: &ServiceRegistry,
plan: ExecutionPlan,
) -> Result<Report, EngineError> {
let context = RuleContext { project, services };
let mut findings = Vec::new();
let mut not_evaluated = Vec::new();
for rule in plan.rules {
let capability = self
.registry
.get(&rule.capability)
.ok_or_else(|| EngineError::UnknownCapability(rule.capability.clone()))?;
let rule_id = rule.id.clone();
let evaluation = capability.evaluate(&context, &rule);
findings.extend(evaluation.findings);
not_evaluated.extend(evaluation.not_evaluated.into_iter().map(|outcome| {
NotEvaluated {
rule_id: rule_id.clone(),
object_id: outcome.object_id,
reason: outcome.reason,
message: outcome.message,
}
}));
}
findings.sort_by(|a, b| {
a.rule_id
.cmp(&b.rule_id)
.then_with(|| a.object_id.cmp(&b.object_id))
.then_with(|| a.message.cmp(&b.message))
});
not_evaluated.sort();
Ok(Report {
findings,
not_evaluated,
})
}
}