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
34impl<'m> Release<'m> {
35 pub(crate) const LEGACY: Release<'static> = Release::Bound(SchemaVersion::Ifc4);
39
40 pub(crate) fn of(model: &'m Model) -> Self {
42 match model.header().schema.as_slice() {
43 [] => Release::LEGACY,
44 [token] => SchemaVersion::from_header_token(token)
45 .map_or(Self::Unsupported(token.as_str()), Self::Bound),
46 tokens => Self::Multiple(tokens.len()),
47 }
48 }
49
50 pub(crate) fn bound(self) -> ClassificationResult<(SchemaVersion, &'static Schema)> {
52 match self {
53 Self::Bound(version) => Ok((
54 version,
55 for_version(version).expect("every SchemaVersion has a bundled table"),
56 )),
57 Self::Multiple(schemas) => Err(ClassificationError::MultipleSchemas { schemas }),
58 Self::Unsupported(schema) => Err(ClassificationError::UnsupportedSchema {
59 schema: schema.to_owned(),
60 }),
61 }
62 }
63
64 pub(crate) fn slot(
70 self,
71 entity: &'static str,
72 id: EntityId,
73 attribute: &'static str,
74 ) -> ClassificationResult<usize> {
75 let (version, schema) = self.bound()?;
76 position(schema, version, entity, attribute)
77 .map(|(slot, _)| slot)
78 .ok_or(ClassificationError::NotInSchema {
79 entity,
80 id,
81 attribute,
82 schema: version,
83 })
84 }
85
86 pub(crate) fn text_slot(
93 self,
94 entity: &'static str,
95 id: EntityId,
96 record: &Entity,
97 attribute: &'static str,
98 ) -> ClassificationResult<usize> {
99 let slot = self.slot(entity, id, attribute)?;
100 if let Some(Value::Ref(target)) = record.attribute(slot) {
101 let declared = self.declared_type(entity, id, attribute)?;
102 let (_, schema) = self.bound()?;
103 if schema.entity(declared).is_some() {
104 return Err(ClassificationError::StructuredValue {
105 entity,
106 id,
107 attribute,
108 target: *target,
109 });
110 }
111 }
112 Ok(slot)
113 }
114
115 pub(crate) fn declared_type(
119 self,
120 entity: &'static str,
121 id: EntityId,
122 attribute: &'static str,
123 ) -> ClassificationResult<&'static str> {
124 let slot = self.slot(entity, id, attribute)?;
125 let (_, schema) = self.bound()?;
126 Ok(schema.attributes(entity)[slot].type_name.as_str())
127 }
128
129 pub(crate) fn accepts(
131 self,
132 entity: &'static str,
133 id: EntityId,
134 attribute: &'static str,
135 candidate: &str,
136 ) -> ClassificationResult<bool> {
137 let declared = self.declared_type(entity, id, attribute)?;
138 let (_, schema) = self.bound()?;
139 Ok(schema.accepts_type(declared, candidate))
140 }
141
142 pub(crate) fn enumerators(
146 self,
147 entity: &'static str,
148 id: EntityId,
149 attribute: &'static str,
150 ) -> ClassificationResult<Vec<&'static str>> {
151 let declared = self.declared_type(entity, id, attribute)?;
152 let (_, schema) = self.bound()?;
153 Ok(enumeration(schema, declared))
154 }
155
156 pub(crate) fn require_entity(
159 self,
160 entity: &'static str,
161 ) -> ClassificationResult<(SchemaVersion, &'static Schema)> {
162 let (version, schema) = self.bound()?;
163 if schema.entity(entity).is_some_and(|e| !e.abstract_) {
164 Ok((version, schema))
165 } else {
166 Err(ClassificationError::EntityNotInSchema {
167 entity,
168 schema: version,
169 })
170 }
171 }
172
173 pub(crate) fn declared(
177 self,
178 entity: &'static str,
179 attribute: &'static str,
180 ) -> ClassificationResult<&'static Attribute> {
181 let (version, schema) = self.require_entity(entity)?;
182 position(schema, version, entity, attribute)
183 .map(|(_, declared)| declared)
184 .ok_or(ClassificationError::AuthoringNotInSchema {
185 entity,
186 attribute,
187 schema: version,
188 })
189 }
190
191 pub(crate) fn record(
201 self,
202 entity: &'static str,
203 values: Vec<(&'static str, Value)>,
204 ) -> ClassificationResult<Entity> {
205 let (version, schema) = self.require_entity(entity)?;
206 let declared = schema.attributes(entity);
207 let mut slots = vec![None; declared.len()];
208 for (attribute, value) in values {
209 match position(schema, version, entity, attribute) {
210 Some((slot, declaration)) => {
211 if holds_text(&value) && !is_text_type(schema, &declaration.type_name) {
212 return Err(ClassificationError::AuthoringValueType {
213 entity,
214 attribute,
215 declared: declaration.type_name.as_str(),
216 schema: version,
217 });
218 }
219 slots[slot] = Some(value);
220 }
221 None if value == Value::Null => {}
222 None => {
223 return Err(ClassificationError::AuthoringNotInSchema {
224 entity,
225 attribute,
226 schema: version,
227 })
228 }
229 }
230 }
231 let mut attributes = Vec::with_capacity(slots.len());
232 for (slot, value) in slots.into_iter().enumerate() {
233 match value.unwrap_or(Value::Null) {
234 Value::Null if !declared[slot].optional => {
235 return Err(ClassificationError::AuthoringRequired {
236 entity,
237 attribute: declared[slot].name.as_str(),
238 schema: version,
239 })
240 }
241 value => attributes.push(value),
242 }
243 }
244 Ok(Entity::new(entity, attributes))
245 }
246}
247
248fn position(
251 schema: &'static Schema,
252 version: SchemaVersion,
253 entity: &str,
254 attribute: &'static str,
255) -> Option<(usize, &'static Attribute)> {
256 let name = release_name(version, entity, attribute);
257 schema
258 .attributes(entity)
259 .into_iter()
260 .enumerate()
261 .find(|(_, declared)| declared.name.eq_ignore_ascii_case(name))
262}
263
264fn holds_text(value: &Value) -> bool {
265 match value {
266 Value::Text(_) => true,
267 Value::List(items) => items.iter().any(holds_text),
268 _ => false,
269 }
270}
271
272fn is_text_type(schema: &Schema, declared: &str) -> bool {
276 schema
277 .resolve_defined(declared)
278 .to_ascii_uppercase()
279 .starts_with("STRING")
280}
281
282pub(crate) fn enumeration(schema: &'static Schema, declared: &str) -> Vec<&'static str> {
285 match schema.type_def(declared).map(|definition| &definition.kind) {
286 Some(TypeKind::Enumeration(values)) => values.iter().map(String::as_str).collect(),
287 _ => Vec::new(),
288 }
289}
290
291pub(crate) fn release_name(
302 release: SchemaVersion,
303 entity: &str,
304 attribute: &'static str,
305) -> &'static str {
306 let entity = entity.to_ascii_uppercase();
307 match (release, entity.as_str(), attribute) {
308 (SchemaVersion::Ifc2x3, "IFCDOCUMENTINFORMATION", "Identification") => "DocumentId",
309 (
310 SchemaVersion::Ifc2x3,
311 "IFCCLASSIFICATIONREFERENCE" | "IFCDOCUMENTREFERENCE" | "IFCLIBRARYREFERENCE",
312 "Identification",
313 ) => "ItemReference",
314 (SchemaVersion::Ifc4x3, "IFCCLASSIFICATION", "Location") => "Specification",
315 _ => attribute,
316 }
317}
318
319pub fn classification_schema(model: &Model) -> ClassificationResult<SchemaVersion> {
334 Release::of(model).bound().map(|(version, _)| version)
335}
336
337#[cfg(test)]
338mod binding_tests;
339#[cfg(test)]
340mod tests;