1use std::sync::Arc;
2
3use axioval_engine::{
4 CompletePropertyAbsenceEvidence, EvidenceSession, EvidenceSessionError, PropertyRequest,
5 PropertyResolution, PropertyResolutionError, PropertyResolutionService,
6 PropertyResolutionServiceHandle, ResolvedProperty, SourceSnapshot,
7};
8use axioval_ir::{Evidence, IrError, Object, ObjectId, Project, Property, PropertyValue, SourceId};
9use ifc_model::{Codec, EntityId, Model};
10use ifc_properties::{
11 ExactPropertyError, ExactResolution, ExactSource, ExactValue, exact_property,
12};
13use ifc_schema::{SchemaVersion, ifc4};
14use ifc_step::StepCodec;
15use sha2::{Digest, Sha256};
16use thiserror::Error;
17
18#[derive(Debug, Error, Eq, PartialEq)]
20pub enum IfcSessionError {
21 #[error("invalid IFC source identity: {0}")]
23 Identity(String),
24 #[error("IFC STEP parse failed: {0}")]
26 Parse(String),
27 #[error("IFC model is incomplete: {diagnostics} parser diagnostics")]
29 IncompleteModel {
30 diagnostics: usize,
32 },
33 #[error("exact IFC sessions require one IFC4 schema declaration, found {0:?}")]
35 UnsupportedSchema(Vec<String>),
36 #[error("failed to construct source-neutral project: {0}")]
38 Project(String),
39 #[error("failed to construct evidence session: {0}")]
41 Session(String),
42}
43
44#[derive(Clone)]
45struct IfcPropertyService {
46 model: Arc<Model>,
47 snapshots: Arc<[SourceSnapshot]>,
48}
49
50impl IfcPropertyService {
51 fn entity_id(request: &PropertyRequest) -> Result<EntityId, PropertyResolutionError> {
52 let local = request
53 .object_id()
54 .local_id
55 .strip_prefix('#')
56 .unwrap_or(&request.object_id().local_id);
57 local
58 .parse::<u64>()
59 .map(EntityId)
60 .map_err(|_| PropertyResolutionError::InvalidRequest)
61 }
62
63 fn locator(&self, detail: impl std::fmt::Display) -> String {
64 format!("ifc:{}:{detail}", self.snapshots[0].fingerprint())
65 }
66}
67
68impl PropertyResolutionService for IfcPropertyService {
69 fn source_snapshots(&self) -> &[SourceSnapshot] {
70 &self.snapshots
71 }
72
73 fn resolve(
74 &self,
75 request: &PropertyRequest,
76 ) -> Result<PropertyResolution, PropertyResolutionError> {
77 if request.object_id().source != *self.snapshots[0].source() {
78 return Err(PropertyResolutionError::InvalidRequest);
79 }
80 let object = Self::entity_id(request)?;
81 match exact_property(
82 &self.model,
83 object,
84 request.property_set(),
85 request.property(),
86 ) {
87 Ok(ExactResolution::Present(exact)) => {
88 let provenance = match exact.source {
89 ExactSource::Occurrence => "occurrence".to_owned(),
90 ExactSource::Type(type_id) => format!("type:{type_id}"),
91 _ => return Err(PropertyResolutionError::InexactEvidence),
92 };
93 if exact.unit_id.is_some() {
94 return Err(PropertyResolutionError::InexactEvidence);
95 }
96 let compatible_type = match (&exact.value, exact.value_type.as_deref()) {
97 (ExactValue::Null, None) => true,
98 (ExactValue::Bool(_), Some(value_type)) => {
99 value_type.eq_ignore_ascii_case("IFCBOOLEAN")
100 }
101 (ExactValue::Integer(_), Some(value_type)) => {
102 value_type.eq_ignore_ascii_case("IFCINTEGER")
103 }
104 (ExactValue::Real(_), Some(value_type)) => {
105 value_type.eq_ignore_ascii_case("IFCREAL")
106 }
107 (ExactValue::Text(_), Some(value_type)) => {
108 ["IFCTEXT", "IFCLABEL", "IFCIDENTIFIER"]
109 .iter()
110 .any(|candidate| value_type.eq_ignore_ascii_case(candidate))
111 }
112 _ => false,
113 };
114 if !compatible_type {
115 return Err(PropertyResolutionError::InexactEvidence);
116 }
117 let value = match exact.value {
118 ExactValue::Null => PropertyValue::Null,
119 ExactValue::Bool(value) => PropertyValue::Boolean(value),
120 ExactValue::Integer(value) => PropertyValue::Integer(value),
121 ExactValue::Real(value) => PropertyValue::Decimal(value),
122 ExactValue::Text(value) => PropertyValue::String(value.to_string()),
123 _ => return Err(PropertyResolutionError::InexactEvidence),
124 };
125 let property =
126 Property::new(exact.property_set.as_ref(), request.property(), value)
127 .map_err(|_| PropertyResolutionError::InvalidRequest)?
128 .with_evidence(Evidence::exact(
129 self.snapshots[0].source().clone(),
130 self.locator(format_args!(
131 "{provenance}:{}/{}",
132 exact.set_id, exact.property_id
133 )),
134 ));
135 Ok(PropertyResolution::Present(ResolvedProperty::try_new(
136 request.clone(),
137 property,
138 )?))
139 }
140 Ok(ExactResolution::Absent) => Ok(PropertyResolution::Absent(
141 CompletePropertyAbsenceEvidence::try_new(
142 request.clone(),
143 Evidence::exact(
144 self.snapshots[0].source().clone(),
145 self.locator(format_args!(
146 "absence:{object}:{}:{}",
147 request.property_set().unwrap_or("*"),
148 request.property()
149 )),
150 ),
151 )?,
152 )),
153 Ok(_) => Err(PropertyResolutionError::InexactEvidence),
154 Err(error) => Err(map_resolution_error(&error)),
155 }
156 }
157}
158
159fn map_resolution_error(error: &ExactPropertyError) -> PropertyResolutionError {
160 match error {
161 ExactPropertyError::IncompleteModel { .. }
162 | ExactPropertyError::MissingReference { .. }
163 | ExactPropertyError::MalformedEntitySlots { .. }
164 | ExactPropertyError::MalformedAggregate { .. }
165 | ExactPropertyError::DuplicateAggregateMember { .. }
166 | ExactPropertyError::MalformedName { .. }
167 | ExactPropertyError::MissingValueSlot { .. }
168 | ExactPropertyError::InvalidOccurrenceTarget { .. }
169 | ExactPropertyError::InvalidTypeTarget { .. } => {
170 PropertyResolutionError::Incomplete(error.to_string())
171 }
172 ExactPropertyError::MultipleTypeAssignments { .. }
173 | ExactPropertyError::DuplicateMatchingSets { .. }
174 | ExactPropertyError::DuplicateMatchingProperties { .. } => {
175 PropertyResolutionError::Conflicting(error.to_string())
176 }
177 ExactPropertyError::UnsupportedDefinition { .. }
178 | ExactPropertyError::UnsupportedProperty { .. }
179 | ExactPropertyError::UnsupportedValue { .. }
180 | ExactPropertyError::UnsupportedUnit { .. }
181 | ExactPropertyError::NonFiniteReal { .. } => PropertyResolutionError::InexactEvidence,
182 _ => PropertyResolutionError::Unavailable(error.to_string()),
183 }
184}
185
186pub fn import_ifc_session(
193 document: impl Into<String>,
194 bytes: &[u8],
195) -> Result<EvidenceSession, IfcSessionError> {
196 let source = SourceId::new("ifc-step", document.into())
197 .map_err(|error| IfcSessionError::Identity(error.to_string()))?;
198 let model = StepCodec
199 .read_bytes(bytes)
200 .map_err(|error| IfcSessionError::Parse(error.to_string()))?;
201 if !model.diagnostics().is_empty() {
202 return Err(IfcSessionError::IncompleteModel {
203 diagnostics: model.diagnostics().len(),
204 });
205 }
206 let schemas = model.header().schema.clone();
207 if !matches!(schemas.as_slice(), [schema] if SchemaVersion::from_header_token(schema) == Some(SchemaVersion::Ifc4))
208 {
209 return Err(IfcSessionError::UnsupportedSchema(schemas));
210 }
211
212 let fingerprint: Arc<str> = Arc::from(format!("sha256:{:x}", Sha256::digest(bytes)));
213 let objects = model
214 .iter()
215 .filter(|(_, entity)| ifc4().is_a(&entity.type_name, "IFCOBJECT"))
216 .map(|(id, entity)| {
217 ObjectId::new(source.clone(), id.to_string())
218 .map(|object_id| Object::new(object_id, entity.type_name.to_string()))
219 })
220 .collect::<Result<Vec<_>, IrError>>()
221 .map_err(|error| IfcSessionError::Project(error.to_string()))?;
222 let project =
223 Project::new(objects).map_err(|error| IfcSessionError::Project(error.to_string()))?;
224 let snapshot =
225 SourceSnapshot::try_new(source.clone(), fingerprint.clone(), fingerprint.clone())
226 .and_then(|snapshot| snapshot.with_schema("IFC4"))
227 .map_err(|error| session_error(&error))?;
228 let service = PropertyResolutionServiceHandle::new(Arc::new(IfcPropertyService {
229 model: Arc::new(model),
230 snapshots: Arc::from([snapshot.clone()]),
231 }));
232 EvidenceSession::try_new(project, [snapshot])
233 .map_err(|error| session_error(&error))?
234 .with_service(service)
235 .map_err(|error| session_error(&error))
236}
237
238fn session_error(error: &EvidenceSessionError) -> IfcSessionError {
239 IfcSessionError::Session(error.to_string())
240}