ifc_lite_server/services/data_model/metadata.rs
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Entity metadata extraction.
6
7use super::generated::{root_attr_indices, RootAttrIndices};
8use super::types::{EntityJob, EntityMetadata};
9use ifc_lite_core::{DecodedEntity, EntityDecoder};
10use rayon::prelude::*;
11use std::sync::Arc;
12
13/// Unknown-type fallback, mirroring the WASM path's
14/// `extractRootAttributesFromEntity`: GlobalId 0, Name 2, Description 3,
15/// ObjectType 4, Tag 7 (the IfcElement/IfcRoot layout), and NO PredefinedType.
16/// Applied only when the type is absent from the schema registry — a KNOWN
17/// type without an attribute keeps its -1 and stays empty on both parse
18/// paths (issue #3949 extended this fallback to GlobalId/Name).
19const UNKNOWN_TYPE_FALLBACK: RootAttrIndices = RootAttrIndices {
20 global_id: 0,
21 name: 2,
22 description: 3,
23 object_type: 4,
24 tag: 7,
25 predefined_type: -1,
26};
27
28/// Non-empty string attribute at a registry index (-1 = not declared).
29fn string_at(entity: &DecodedEntity, idx: i8) -> Option<String> {
30 if idx < 0 {
31 return None;
32 }
33 entity
34 .get_string(idx as usize)
35 .filter(|s| !s.is_empty())
36 .map(|s| s.to_string())
37}
38
39/// PredefinedType enum token at a registry index. STEP enums decode to
40/// `AttributeValue::Enum` with the dots already stripped (`.SOLIDWALL.` →
41/// `SOLIDWALL`), matching the WASM `extractAllEntityAttributes` display value —
42/// read via `as_enum()`, never `get_string()`.
43fn enum_at(entity: &DecodedEntity, idx: i8) -> Option<String> {
44 if idx < 0 {
45 return None;
46 }
47 entity
48 .get(idx as usize)
49 .and_then(|v| v.as_enum())
50 .filter(|s| !s.is_empty())
51 .map(|s| s.to_string())
52}
53
54/// Extract entity metadata for all entities.
55pub(super) fn extract_entity_metadata(
56 jobs: &[EntityJob],
57 content: &Arc<Vec<u8>>,
58 entity_index: &Arc<ifc_lite_core::EntityIndex>,
59) -> Vec<EntityMetadata> {
60 jobs.par_iter()
61 .filter_map(|job| {
62 let mut local_decoder =
63 EntityDecoder::with_arc_index(content.as_slice(), entity_index.clone());
64 let entity = local_decoder.decode_at(job.start, job.end).ok()?;
65 let has_geometry = ifc_lite_core::has_geometry_by_name(&job.type_name);
66
67 // Root attributes at the SAME schema-registry positions the WASM
68 // path resolves them (issue #1765) — see generated/attr_indices.rs.
69 // GlobalId/Name are NOT fixed at 0/2 for every entity: that only
70 // holds for `IfcRoot` subtypes. `IfcClassification`, for example,
71 // has no GlobalId at all and its Name sits at index 3, not 2
72 // (issue #3949).
73 let upper = job.type_name.to_uppercase();
74 let idx = root_attr_indices(&upper).unwrap_or(UNKNOWN_TYPE_FALLBACK);
75 let global_id = string_at(&entity, idx.global_id);
76 let name = string_at(&entity, idx.name);
77 let description = string_at(&entity, idx.description);
78 let object_type = string_at(&entity, idx.object_type);
79 let tag = string_at(&entity, idx.tag);
80 let predefined_type = enum_at(&entity, idx.predefined_type);
81
82 Some(EntityMetadata {
83 entity_id: job.id,
84 type_name: job.type_name.clone(),
85 global_id,
86 name,
87 description,
88 object_type,
89 tag,
90 predefined_type,
91 has_geometry,
92 })
93 })
94 .collect()
95}