axioval_openbim/
ifc.rs

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/// Production IFC import/session construction failure.
19#[derive(Debug, Error, Eq, PartialEq)]
20pub enum IfcSessionError {
21    /// Source document identity was blank or otherwise invalid.
22    #[error("invalid IFC source identity: {0}")]
23    Identity(String),
24    /// Strict STEP parsing failed.
25    #[error("IFC STEP parse failed: {0}")]
26    Parse(String),
27    /// The parser recovered with diagnostics, so the snapshot is incomplete.
28    #[error("IFC model is incomplete: {diagnostics} parser diagnostics")]
29    IncompleteModel {
30        /// Number of source diagnostics retained by the parser.
31        diagnostics: usize,
32    },
33    /// The file did not declare exactly one supported IFC4 schema.
34    #[error("exact IFC sessions require one IFC4 schema declaration, found {0:?}")]
35    UnsupportedSchema(Vec<String>),
36    /// Source-neutral project construction failed.
37    #[error("failed to construct source-neutral project: {0}")]
38    Project(String),
39    /// Immutable snapshot/session binding failed.
40    #[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
186/// Parses strict IFC STEP bytes and binds an immutable exact-evidence session.
187///
188/// # Errors
189///
190/// Returns [`IfcSessionError`] when identity, strict parsing, schema validation,
191/// source-neutral project construction, snapshot binding, or service registration fails.
192pub 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}