Skip to main content

ifc_classification/
view.rs

1//! Shared borrowed-view and strict positional decoding helpers.
2
3use std::collections::HashSet;
4
5use crate::release::Release;
6use crate::{ClassificationError, ClassificationResult};
7use ifc_model::{Entity, EntityId, Model, Value};
8
9/// Entry point for classification/document/library queries over a borrowed [`Model`].
10///
11/// Every projection the view hands out reads against the release the model's
12/// header declares (see [`crate::classification_schema`]).
13#[derive(Debug, Clone, Copy)]
14pub struct ClassificationView<'m> {
15    model: &'m Model,
16}
17impl<'m> ClassificationView<'m> {
18    /// Borrow `model` for classification-schema queries.
19    #[must_use]
20    pub const fn new(model: &'m Model) -> Self {
21        Self { model }
22    }
23    #[must_use]
24    pub(crate) const fn model(self) -> &'m Model {
25        self.model
26    }
27    /// The release this view's projections read against.
28    pub(crate) fn release(self) -> Release<'m> {
29        Release::of(self.model)
30    }
31}
32
33macro_rules! borrowed_entity {
34    ($name:ident, $kind:literal) => {
35        #[doc = concat!("Borrowed projection of an `", $kind, "` entity.")]
36        #[doc = ""]
37        #[doc = "Projections handed out by a [`crate::ClassificationView`] read against"]
38        #[doc = "the release the model declares (IFC2X3, IFC4 or IFC4X3); one built with"]
39        #[doc = "`try_new` has no model and reads against IFC4, as in 0.2.0."]
40        #[derive(Debug, Clone, Copy)]
41        pub struct $name<'m> {
42            id: ifc_model::EntityId,
43            entity: &'m ifc_model::Entity,
44            release: crate::release::Release<'m>,
45        }
46        impl<'m> $name<'m> {
47            #[doc = concat!(
48                        "Project `entity` as `",
49                        $kind,
50                        "`, checking its runtime type; fails with `WrongEntityType` if it is not."
51                    )]
52            #[doc = ""]
53            #[doc = "Without a model there is no header to bind, so the projection reads"]
54            #[doc = "against IFC4. Use the view's lookups to read another release."]
55            pub fn try_new(
56                id: ifc_model::EntityId,
57                entity: &'m ifc_model::Entity,
58            ) -> crate::ClassificationResult<Self> {
59                Self::try_bound(id, entity, crate::release::Release::LEGACY)
60            }
61            /// [`Self::try_new`] against an explicit release binding.
62            pub(crate) fn try_bound(
63                id: ifc_model::EntityId,
64                entity: &'m ifc_model::Entity,
65                release: crate::release::Release<'m>,
66            ) -> crate::ClassificationResult<Self> {
67                if entity.is_type($kind) {
68                    Ok(Self {
69                        id,
70                        entity,
71                        release,
72                    })
73                } else {
74                    Err(crate::ClassificationError::WrongEntityType {
75                        expected: $kind,
76                        actual: entity.type_name.to_string(),
77                    })
78                }
79            }
80            #[doc = concat!(
81                        "Wrap `entity` as `",
82                        $kind,
83                        "` without re-checking its type; caller must already know it matches."
84                    )]
85            pub(crate) const fn from_known(
86                id: ifc_model::EntityId,
87                entity: &'m ifc_model::Entity,
88                release: crate::release::Release<'m>,
89            ) -> Self {
90                Self {
91                    id,
92                    entity,
93                    release,
94                }
95            }
96            /// Entity id of the underlying instance.
97            #[must_use]
98            pub const fn id(self) -> ifc_model::EntityId {
99                self.id
100            }
101            /// Borrowed underlying entity record.
102            #[must_use]
103            pub const fn entity(self) -> &'m ifc_model::Entity {
104                self.entity
105            }
106            /// Slot of `attribute` (IFC4 name) in the bound release.
107            #[allow(dead_code)]
108            pub(crate) fn slot(
109                self,
110                attribute: &'static str,
111            ) -> crate::ClassificationResult<usize> {
112                self.release.slot($kind, self.id, attribute)
113            }
114            /// Slot of a text `attribute`; a record-typed value is `StructuredValue`.
115            #[allow(dead_code)]
116            pub(crate) fn text_slot(
117                self,
118                attribute: &'static str,
119            ) -> crate::ClassificationResult<usize> {
120                self.release
121                    .text_slot($kind, self.id, self.entity, attribute)
122            }
123            /// The release binding of this projection.
124            #[allow(dead_code)]
125            pub(crate) const fn release(self) -> crate::release::Release<'m> {
126                self.release
127            }
128        }
129    };
130}
131pub(crate) use borrowed_entity;
132
133fn invalid(
134    entity: &'static str,
135    id: EntityId,
136    attribute: &'static str,
137    value: &Value,
138) -> ClassificationError {
139    ClassificationError::InvalidValue {
140        entity,
141        id,
142        attribute,
143        value: format!("{value:?}"),
144    }
145}
146
147fn unique_refs(
148    kind: &'static str,
149    id: EntityId,
150    attr: &'static str,
151    values: &[Value],
152) -> ClassificationResult<Vec<EntityId>> {
153    let mut seen = HashSet::new();
154    let mut out = Vec::with_capacity(values.len());
155    for value in values {
156        let Value::Ref(target) = value else {
157            return Err(invalid(kind, id, attr, value));
158        };
159        if !seen.insert(*target) {
160            return Err(ClassificationError::InvalidValue {
161                entity: kind,
162                id,
163                attribute: attr,
164                value: format!("duplicate reference {target}"),
165            });
166        }
167        out.push(*target);
168    }
169    Ok(out)
170}
171/// Decode a required text attribute at `slot`; fails if unset or not text.
172pub(crate) fn required_text<'m>(
173    kind: &'static str,
174    id: EntityId,
175    e: &'m Entity,
176    slot: usize,
177    attr: &'static str,
178) -> ClassificationResult<&'m str> {
179    match e.attribute(slot) {
180        None | Some(Value::Null) => Err(ClassificationError::MissingAttribute {
181            entity: kind,
182            id,
183            attribute: attr,
184        }),
185        Some(v) => v
186            .unwrap_typed()
187            .as_text()
188            .ok_or_else(|| invalid(kind, id, attr, v)),
189    }
190}
191/// Decode an optional text attribute at `slot`; `None` when unset, fails if present but not text.
192pub(crate) fn optional_text<'m>(
193    kind: &'static str,
194    id: EntityId,
195    e: &'m Entity,
196    slot: usize,
197    attr: &'static str,
198) -> ClassificationResult<Option<&'m str>> {
199    match e.attribute(slot) {
200        None | Some(Value::Null) => Ok(None),
201        Some(v) => v
202            .unwrap_typed()
203            .as_text()
204            .map(Some)
205            .ok_or_else(|| invalid(kind, id, attr, v)),
206    }
207}
208/// Decode an optional enumeration attribute at `slot`, restricted to `allowed`; fails on any other value.
209pub(crate) fn optional_enum<'m>(
210    kind: &'static str,
211    id: EntityId,
212    e: &'m Entity,
213    slot: usize,
214    attr: &'static str,
215    allowed: &[&str],
216) -> ClassificationResult<Option<&'m str>> {
217    match e.attribute(slot) {
218        None | Some(Value::Null) => Ok(None),
219        Some(Value::Enum(v)) if allowed.iter().any(|a| v.eq_ignore_ascii_case(a)) => Ok(Some(v)),
220        Some(v) => Err(invalid(kind, id, attr, v)),
221    }
222}
223/// Decode an optional single reference attribute at `slot`; `None` when unset, fails if present but not a reference.
224pub(crate) fn optional_ref(
225    kind: &'static str,
226    id: EntityId,
227    e: &Entity,
228    slot: usize,
229    attr: &'static str,
230) -> ClassificationResult<Option<EntityId>> {
231    match e.attribute(slot) {
232        None | Some(Value::Null) => Ok(None),
233        Some(Value::Ref(target)) => Ok(Some(*target)),
234        Some(v) => Err(invalid(kind, id, attr, v)),
235    }
236}
237/// Decode a required single reference attribute at `slot`; fails if unset or not a reference.
238pub(crate) fn required_ref(
239    kind: &'static str,
240    id: EntityId,
241    e: &Entity,
242    slot: usize,
243    attr: &'static str,
244) -> ClassificationResult<EntityId> {
245    optional_ref(kind, id, e, slot, attr)?.ok_or(ClassificationError::MissingAttribute {
246        entity: kind,
247        id,
248        attribute: attr,
249    })
250}
251/// Decode a required, non-empty list of unique references at `slot`; fails if unset, empty, non-reference, or containing duplicates.
252pub(crate) fn required_refs(
253    kind: &'static str,
254    id: EntityId,
255    e: &Entity,
256    slot: usize,
257    attr: &'static str,
258) -> ClassificationResult<Vec<EntityId>> {
259    let value = e
260        .attribute(slot)
261        .ok_or(ClassificationError::MissingAttribute {
262            entity: kind,
263            id,
264            attribute: attr,
265        })?;
266    let Value::List(values) = value else {
267        return Err(invalid(kind, id, attr, value));
268    };
269    if values.is_empty() {
270        return Err(invalid(kind, id, attr, value));
271    }
272    unique_refs(kind, id, attr, values)
273}
274/// Decode an optional, non-empty list of unique references at `slot`; `None` when unset, fails on duplicates or non-references.
275pub(crate) fn optional_refs(
276    kind: &'static str,
277    id: EntityId,
278    e: &Entity,
279    slot: usize,
280    attr: &'static str,
281) -> ClassificationResult<Option<Vec<EntityId>>> {
282    match e.attribute(slot) {
283        None | Some(Value::Null) => Ok(None),
284        Some(Value::List(values)) if !values.is_empty() => {
285            unique_refs(kind, id, attr, values).map(Some)
286        }
287        Some(v) => Err(invalid(kind, id, attr, v)),
288    }
289}
290/// Decode an optional, non-empty list of text values at `slot`; `None` when unset, fails if any element is not text.
291pub(crate) fn optional_texts<'m>(
292    kind: &'static str,
293    id: EntityId,
294    e: &'m Entity,
295    slot: usize,
296    attr: &'static str,
297) -> ClassificationResult<Option<Vec<&'m str>>> {
298    match e.attribute(slot) {
299        None | Some(Value::Null) => Ok(None),
300        Some(Value::List(values)) if !values.is_empty() => values
301            .iter()
302            .map(|v| {
303                v.unwrap_typed()
304                    .as_text()
305                    .ok_or_else(|| invalid(kind, id, attr, v))
306            })
307            .collect::<ClassificationResult<Vec<_>>>()
308            .map(Some),
309        Some(v) => Err(invalid(kind, id, attr, v)),
310    }
311}