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::{ClassificationError, ClassificationResult};
6use ifc_model::{Entity, EntityId, Model, Value};
7
8/// Entry point for classification/document/library queries over a borrowed [`Model`].
9#[derive(Debug, Clone, Copy)]
10pub struct ClassificationView<'m> {
11    model: &'m Model,
12}
13impl<'m> ClassificationView<'m> {
14    /// Borrow `model` for classification-schema queries.
15    #[must_use]
16    pub const fn new(model: &'m Model) -> Self {
17        Self { model }
18    }
19    #[must_use]
20    pub(crate) const fn model(self) -> &'m Model {
21        self.model
22    }
23}
24
25macro_rules! borrowed_entity {
26    ($name:ident, $kind:literal) => {
27        #[doc = concat!("Borrowed projection of an `", $kind, "` entity.")]
28        #[derive(Debug, Clone, Copy)]
29        pub struct $name<'m> {
30            id: ifc_model::EntityId,
31            entity: &'m ifc_model::Entity,
32        }
33        impl<'m> $name<'m> {
34            #[doc = concat!(
35                        "Project `entity` as `",
36                        $kind,
37                        "`, checking its runtime type; fails with `WrongEntityType` if it is not."
38                    )]
39            pub fn try_new(
40                id: ifc_model::EntityId,
41                entity: &'m ifc_model::Entity,
42            ) -> crate::ClassificationResult<Self> {
43                if entity.is_type($kind) {
44                    Ok(Self { id, entity })
45                } else {
46                    Err(crate::ClassificationError::WrongEntityType {
47                        expected: $kind,
48                        actual: entity.type_name.to_string(),
49                    })
50                }
51            }
52            #[doc = concat!(
53                        "Wrap `entity` as `",
54                        $kind,
55                        "` without re-checking its type; caller must already know it matches."
56                    )]
57            pub(crate) const fn from_known(
58                id: ifc_model::EntityId,
59                entity: &'m ifc_model::Entity,
60            ) -> Self {
61                Self { id, entity }
62            }
63            /// Entity id of the underlying instance.
64            #[must_use]
65            pub const fn id(self) -> ifc_model::EntityId {
66                self.id
67            }
68            /// Borrowed underlying entity record.
69            #[must_use]
70            pub const fn entity(self) -> &'m ifc_model::Entity {
71                self.entity
72            }
73        }
74    };
75}
76pub(crate) use borrowed_entity;
77
78fn invalid(
79    entity: &'static str,
80    id: EntityId,
81    attribute: &'static str,
82    value: &Value,
83) -> ClassificationError {
84    ClassificationError::InvalidValue {
85        entity,
86        id,
87        attribute,
88        value: format!("{value:?}"),
89    }
90}
91
92fn unique_refs(
93    kind: &'static str,
94    id: EntityId,
95    attr: &'static str,
96    values: &[Value],
97) -> ClassificationResult<Vec<EntityId>> {
98    let mut seen = HashSet::new();
99    let mut out = Vec::with_capacity(values.len());
100    for value in values {
101        let Value::Ref(target) = value else {
102            return Err(invalid(kind, id, attr, value));
103        };
104        if !seen.insert(*target) {
105            return Err(ClassificationError::InvalidValue {
106                entity: kind,
107                id,
108                attribute: attr,
109                value: format!("duplicate reference {target}"),
110            });
111        }
112        out.push(*target);
113    }
114    Ok(out)
115}
116/// Decode a required text attribute at `slot`; fails if unset or not text.
117pub(crate) fn required_text<'m>(
118    kind: &'static str,
119    id: EntityId,
120    e: &'m Entity,
121    slot: usize,
122    attr: &'static str,
123) -> ClassificationResult<&'m str> {
124    match e.attribute(slot) {
125        None | Some(Value::Null) => Err(ClassificationError::MissingAttribute {
126            entity: kind,
127            id,
128            attribute: attr,
129        }),
130        Some(v) => v
131            .unwrap_typed()
132            .as_text()
133            .ok_or_else(|| invalid(kind, id, attr, v)),
134    }
135}
136/// Decode an optional text attribute at `slot`; `None` when unset, fails if present but not text.
137pub(crate) fn optional_text<'m>(
138    kind: &'static str,
139    id: EntityId,
140    e: &'m Entity,
141    slot: usize,
142    attr: &'static str,
143) -> ClassificationResult<Option<&'m str>> {
144    match e.attribute(slot) {
145        None | Some(Value::Null) => Ok(None),
146        Some(v) => v
147            .unwrap_typed()
148            .as_text()
149            .map(Some)
150            .ok_or_else(|| invalid(kind, id, attr, v)),
151    }
152}
153/// Decode an optional enumeration attribute at `slot`, restricted to `allowed`; fails on any other value.
154pub(crate) fn optional_enum<'m>(
155    kind: &'static str,
156    id: EntityId,
157    e: &'m Entity,
158    slot: usize,
159    attr: &'static str,
160    allowed: &[&str],
161) -> ClassificationResult<Option<&'m str>> {
162    match e.attribute(slot) {
163        None | Some(Value::Null) => Ok(None),
164        Some(Value::Enum(v)) if allowed.iter().any(|a| v.eq_ignore_ascii_case(a)) => Ok(Some(v)),
165        Some(v) => Err(invalid(kind, id, attr, v)),
166    }
167}
168/// Decode an optional single reference attribute at `slot`; `None` when unset, fails if present but not a reference.
169pub(crate) fn optional_ref(
170    kind: &'static str,
171    id: EntityId,
172    e: &Entity,
173    slot: usize,
174    attr: &'static str,
175) -> ClassificationResult<Option<EntityId>> {
176    match e.attribute(slot) {
177        None | Some(Value::Null) => Ok(None),
178        Some(Value::Ref(target)) => Ok(Some(*target)),
179        Some(v) => Err(invalid(kind, id, attr, v)),
180    }
181}
182/// Decode a required single reference attribute at `slot`; fails if unset or not a reference.
183pub(crate) fn required_ref(
184    kind: &'static str,
185    id: EntityId,
186    e: &Entity,
187    slot: usize,
188    attr: &'static str,
189) -> ClassificationResult<EntityId> {
190    optional_ref(kind, id, e, slot, attr)?.ok_or(ClassificationError::MissingAttribute {
191        entity: kind,
192        id,
193        attribute: attr,
194    })
195}
196/// Decode a required, non-empty list of unique references at `slot`; fails if unset, empty, non-reference, or containing duplicates.
197pub(crate) fn required_refs(
198    kind: &'static str,
199    id: EntityId,
200    e: &Entity,
201    slot: usize,
202    attr: &'static str,
203) -> ClassificationResult<Vec<EntityId>> {
204    let value = e
205        .attribute(slot)
206        .ok_or(ClassificationError::MissingAttribute {
207            entity: kind,
208            id,
209            attribute: attr,
210        })?;
211    let Value::List(values) = value else {
212        return Err(invalid(kind, id, attr, value));
213    };
214    if values.is_empty() {
215        return Err(invalid(kind, id, attr, value));
216    }
217    unique_refs(kind, id, attr, values)
218}
219/// Decode an optional, non-empty list of unique references at `slot`; `None` when unset, fails on duplicates or non-references.
220pub(crate) fn optional_refs(
221    kind: &'static str,
222    id: EntityId,
223    e: &Entity,
224    slot: usize,
225    attr: &'static str,
226) -> ClassificationResult<Option<Vec<EntityId>>> {
227    match e.attribute(slot) {
228        None | Some(Value::Null) => Ok(None),
229        Some(Value::List(values)) if !values.is_empty() => {
230            unique_refs(kind, id, attr, values).map(Some)
231        }
232        Some(v) => Err(invalid(kind, id, attr, v)),
233    }
234}
235/// Decode an optional, non-empty list of text values at `slot`; `None` when unset, fails if any element is not text.
236pub(crate) fn optional_texts<'m>(
237    kind: &'static str,
238    id: EntityId,
239    e: &'m Entity,
240    slot: usize,
241    attr: &'static str,
242) -> ClassificationResult<Option<Vec<&'m str>>> {
243    match e.attribute(slot) {
244        None | Some(Value::Null) => Ok(None),
245        Some(Value::List(values)) if !values.is_empty() => values
246            .iter()
247            .map(|v| {
248                v.unwrap_typed()
249                    .as_text()
250                    .ok_or_else(|| invalid(kind, id, attr, v))
251            })
252            .collect::<ClassificationResult<Vec<_>>>()
253            .map(Some),
254        Some(v) => Err(invalid(kind, id, attr, v)),
255    }
256}