1use std::collections::HashSet;
4
5use crate::{ClassificationError, ClassificationResult};
6use ifc_model::{Entity, EntityId, Model, Value};
7
8#[derive(Debug, Clone, Copy)]
10pub struct ClassificationView<'m> {
11 model: &'m Model,
12}
13impl<'m> ClassificationView<'m> {
14 #[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 #[must_use]
65 pub const fn id(self) -> ifc_model::EntityId {
66 self.id
67 }
68 #[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}
116pub(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}
136pub(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}
153pub(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}
168pub(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}
182pub(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}
196pub(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}
219pub(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}
235pub(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}