1use ifc_model::{Entity, EntityId, Model, Value};
19use ifc_schema::{for_version, Attribute, Schema, SchemaVersion, TypeKind};
20
21use crate::{ClassificationError, ClassificationResult};
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub(crate) enum Release<'m> {
26 Bound(SchemaVersion),
28 Multiple(usize),
30 Unsupported(&'m str),
32}
33
34const fn proven(version: SchemaVersion) -> bool {
40 matches!(
41 version,
42 SchemaVersion::Ifc2x3 | SchemaVersion::Ifc4 | SchemaVersion::Ifc4x3
43 )
44}
45
46impl<'m> Release<'m> {
47 pub(crate) const LEGACY: Release<'static> = Release::Bound(SchemaVersion::Ifc4);
51
52 pub(crate) fn of(model: &'m Model) -> Self {
54 match model.header().schema.as_slice() {
55 [] => Release::LEGACY,
56 [token] => SchemaVersion::from_header_token(token)
57 .filter(|version| proven(*version))
58 .map_or(Self::Unsupported(token.as_str()), Self::Bound),
59 tokens => Self::Multiple(tokens.len()),
60 }
61 }
62
63 pub(crate) fn bound(self) -> ClassificationResult<(SchemaVersion, &'static Schema)> {
65 match self {
66 Self::Bound(version) => Ok((
67 version,
68 for_version(version).expect("every SchemaVersion has a bundled table"),
69 )),
70 Self::Multiple(schemas) => Err(ClassificationError::MultipleSchemas { schemas }),
71 Self::Unsupported(schema) => Err(ClassificationError::UnsupportedSchema {
72 schema: schema.to_owned(),
73 }),
74 }
75 }
76
77 pub(crate) fn slot(
83 self,
84 entity: &'static str,
85 id: EntityId,
86 attribute: &'static str,
87 ) -> ClassificationResult<usize> {
88 let (version, schema) = self.bound()?;
89 position(schema, version, entity, attribute)
90 .map(|(slot, _)| slot)
91 .ok_or(ClassificationError::NotInSchema {
92 entity,
93 id,
94 attribute,
95 schema: version,
96 })
97 }
98
99 pub(crate) fn text_slot(
106 self,
107 entity: &'static str,
108 id: EntityId,
109 record: &Entity,
110 attribute: &'static str,
111 ) -> ClassificationResult<usize> {
112 let slot = self.slot(entity, id, attribute)?;
113 if let Some(Value::Ref(target)) = record.attribute(slot) {
114 let declared = self.declared_type(entity, id, attribute)?;
115 let (_, schema) = self.bound()?;
116 if schema.entity(declared).is_some() {
117 return Err(ClassificationError::StructuredValue {
118 entity,
119 id,
120 attribute,
121 target: *target,
122 });
123 }
124 }
125 Ok(slot)
126 }
127
128 pub(crate) fn declared_type(
132 self,
133 entity: &'static str,
134 id: EntityId,
135 attribute: &'static str,
136 ) -> ClassificationResult<&'static str> {
137 let slot = self.slot(entity, id, attribute)?;
138 let (_, schema) = self.bound()?;
139 Ok(schema.attributes(entity)[slot].type_name.as_str())
140 }
141
142 pub(crate) fn accepts(
144 self,
145 entity: &'static str,
146 id: EntityId,
147 attribute: &'static str,
148 candidate: &str,
149 ) -> ClassificationResult<bool> {
150 let declared = self.declared_type(entity, id, attribute)?;
151 let (_, schema) = self.bound()?;
152 Ok(schema.accepts_type(declared, candidate))
153 }
154
155 pub(crate) fn enumerators(
159 self,
160 entity: &'static str,
161 id: EntityId,
162 attribute: &'static str,
163 ) -> ClassificationResult<Vec<&'static str>> {
164 let declared = self.declared_type(entity, id, attribute)?;
165 let (_, schema) = self.bound()?;
166 Ok(enumeration(schema, declared))
167 }
168
169 pub(crate) fn require_entity(
172 self,
173 entity: &'static str,
174 ) -> ClassificationResult<(SchemaVersion, &'static Schema)> {
175 let (version, schema) = self.bound()?;
176 if schema.entity(entity).is_some_and(|e| !e.abstract_) {
177 Ok((version, schema))
178 } else {
179 Err(ClassificationError::EntityNotInSchema {
180 entity,
181 schema: version,
182 })
183 }
184 }
185
186 pub(crate) fn declared(
190 self,
191 entity: &'static str,
192 attribute: &'static str,
193 ) -> ClassificationResult<&'static Attribute> {
194 let (version, schema) = self.require_entity(entity)?;
195 position(schema, version, entity, attribute)
196 .map(|(_, declared)| declared)
197 .ok_or(ClassificationError::AuthoringNotInSchema {
198 entity,
199 attribute,
200 schema: version,
201 })
202 }
203
204 pub(crate) fn record(
214 self,
215 entity: &'static str,
216 values: Vec<(&'static str, Value)>,
217 ) -> ClassificationResult<Entity> {
218 let (version, schema) = self.require_entity(entity)?;
219 let declared = schema.attributes(entity);
220 let mut slots = vec![None; declared.len()];
221 for (attribute, value) in values {
222 match position(schema, version, entity, attribute) {
223 Some((slot, declaration)) => {
224 if holds_text(&value) && !is_text_type(schema, &declaration.type_name) {
225 return Err(ClassificationError::AuthoringValueType {
226 entity,
227 attribute,
228 declared: declaration.type_name.as_str(),
229 schema: version,
230 });
231 }
232 slots[slot] = Some(value);
233 }
234 None if value == Value::Null => {}
235 None => {
236 return Err(ClassificationError::AuthoringNotInSchema {
237 entity,
238 attribute,
239 schema: version,
240 })
241 }
242 }
243 }
244 let mut attributes = Vec::with_capacity(slots.len());
245 for (slot, value) in slots.into_iter().enumerate() {
246 match value.unwrap_or(Value::Null) {
247 Value::Null if !declared[slot].optional => {
248 return Err(ClassificationError::AuthoringRequired {
249 entity,
250 attribute: declared[slot].name.as_str(),
251 schema: version,
252 })
253 }
254 value => attributes.push(value),
255 }
256 }
257 Ok(Entity::new(entity, attributes))
258 }
259}
260
261fn position(
264 schema: &'static Schema,
265 version: SchemaVersion,
266 entity: &str,
267 attribute: &'static str,
268) -> Option<(usize, &'static Attribute)> {
269 let name = release_name(version, entity, attribute);
270 schema
271 .attributes(entity)
272 .into_iter()
273 .enumerate()
274 .find(|(_, declared)| declared.name.eq_ignore_ascii_case(name))
275}
276
277fn holds_text(value: &Value) -> bool {
278 match value {
279 Value::Text(_) => true,
280 Value::List(items) => items.iter().any(holds_text),
281 _ => false,
282 }
283}
284
285fn is_text_type(schema: &Schema, declared: &str) -> bool {
289 schema
290 .resolve_defined(declared)
291 .to_ascii_uppercase()
292 .starts_with("STRING")
293}
294
295pub(crate) fn enumeration(schema: &'static Schema, declared: &str) -> Vec<&'static str> {
298 match schema.type_def(declared).map(|definition| &definition.kind) {
299 Some(TypeKind::Enumeration(values)) => values.iter().map(String::as_str).collect(),
300 _ => Vec::new(),
301 }
302}
303
304pub(crate) fn release_name(
315 release: SchemaVersion,
316 entity: &str,
317 attribute: &'static str,
318) -> &'static str {
319 let entity = entity.to_ascii_uppercase();
320 match (release, entity.as_str(), attribute) {
321 (SchemaVersion::Ifc2x3, "IFCDOCUMENTINFORMATION", "Identification") => "DocumentId",
322 (
323 SchemaVersion::Ifc2x3,
324 "IFCCLASSIFICATIONREFERENCE" | "IFCDOCUMENTREFERENCE" | "IFCLIBRARYREFERENCE",
325 "Identification",
326 ) => "ItemReference",
327 (SchemaVersion::Ifc4x3, "IFCCLASSIFICATION", "Location") => "Specification",
328 _ => attribute,
329 }
330}
331
332pub fn classification_schema(model: &Model) -> ClassificationResult<SchemaVersion> {
348 Release::of(model).bound().map(|(version, _)| version)
349}
350
351#[cfg(test)]
352mod binding_tests;
353#[cfg(test)]
354mod tests;
355
356#[cfg(test)]
357mod intermediate_release_tests {
358 use super::*;
359
360 #[test]
363 fn ifc4x1_and_ifc4x2_are_refused_not_aliased() {
364 for token in ["IFC4X1", "IFC4X2"] {
365 let mut model = Model::new();
366 model.header_mut().schema = vec![token.to_owned()];
367 assert!(
368 matches!(Release::of(&model).bound(), Err(ClassificationError::UnsupportedSchema { schema }) if schema == token),
369 "{token} must be refused"
370 );
371 }
372 }
373}