1use std::collections::HashSet;
4
5use crate::release::Release;
6use crate::{ClassificationError, ClassificationResult};
7use ifc_model::{Entity, EntityId, Model, Value};
8
9#[derive(Debug, Clone, Copy)]
14pub struct ClassificationView<'m> {
15 model: &'m Model,
16}
17impl<'m> ClassificationView<'m> {
18 #[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 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 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 #[must_use]
98 pub const fn id(self) -> ifc_model::EntityId {
99 self.id
100 }
101 #[must_use]
103 pub const fn entity(self) -> &'m ifc_model::Entity {
104 self.entity
105 }
106 #[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 #[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 #[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}
171pub(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}
191pub(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}
208pub(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}
223pub(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}
237pub(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}
251pub(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}
274pub(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}
290pub(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}