Skip to main content

ifc_author/builder/
entity.rs

1//! Build one entity by naming its attributes.
2//!
3//! # The problem this solves
4//!
5//! `Model::push` takes a positional `Vec<Value>`. Authoring an `IfcAnnotation`
6//! therefore means knowing that it has seven slots, that slot 0 is the
7//! GlobalId, slot 2 the name, and that slots 1 and 3..6 are optional. Get the
8//! count wrong and the file still writes -- and is rejected downstream.
9//!
10//! Here the caller names attributes and the schema decides the positions.
11//!
12//! # Attribute names match case-insensitively
13//!
14//! Names are compared with `eq_ignore_ascii_case`, so an IFC4 and IFC4X3
15//! spelling that differs only in case resolves to the same slot and is not a
16//! bug. A true rename between releases is a different name and is refused as
17//! unknown in the release that lacks it.
18
19use ifc_model::{Entity, Value};
20use ifc_schema::Schema;
21
22use crate::check::{aggregate_element, describe_value, is_derived_slot, judge_value, Verdict};
23use crate::error::{AuthorError, AuthorResult};
24
25/// A partially-specified entity, checked against the schema on [`build`].
26///
27/// [`build`]: EntityBuilder::build
28#[derive(Debug, Clone)]
29pub struct EntityBuilder<'a> {
30    schema: &'a Schema,
31    entity: String,
32    /// Attribute name and value, in the order the caller set them. Kept as a
33    /// list rather than a map so a duplicate set is detectable and the error
34    /// can name the attribute.
35    set: Vec<(String, Value)>,
36}
37
38impl<'a> EntityBuilder<'a> {
39    /// Start building `entity`, whose type must be declared by `schema`.
40    ///
41    /// The type is not checked here: an unknown type is reported by [`build`],
42    /// so that a caller assembling several entities gets every failure at the
43    /// same point in its code rather than some at construction and some later.
44    ///
45    /// [`build`]: EntityBuilder::build
46    pub fn new(schema: &'a Schema, entity: impl Into<String>) -> Self {
47        Self {
48            schema,
49            entity: entity.into(),
50            set: Vec::new(),
51        }
52    }
53
54    /// Set one attribute by its declared name.
55    ///
56    /// Names are matched case-insensitively, because EXPRESS declares
57    /// `GlobalId` while STEP files and much prose write `GLOBALID`.
58    #[must_use]
59    pub fn set(mut self, attribute: impl Into<String>, value: Value) -> Self {
60        self.set.push((attribute.into(), value));
61        self
62    }
63
64    /// Set a text attribute.
65    pub fn text(self, attribute: impl Into<String>, text: impl Into<std::sync::Arc<str>>) -> Self {
66        self.set(attribute, Value::Text(text.into()))
67    }
68
69    /// Set a real-valued attribute.
70    pub fn real(self, attribute: impl Into<String>, value: f64) -> Self {
71        self.set(attribute, Value::Real(value))
72    }
73
74    /// Set an entity-reference attribute.
75    pub fn reference(self, attribute: impl Into<String>, id: ifc_model::EntityId) -> Self {
76        self.set(attribute, Value::Ref(id))
77    }
78
79    /// Set an enumeration attribute (written `.PLAN_VIEW.` in STEP).
80    pub fn enumeration(
81        self,
82        attribute: impl Into<String>,
83        constant: impl Into<std::sync::Arc<str>>,
84    ) -> Self {
85        self.set(attribute, Value::Enum(constant.into()))
86    }
87}
88
89impl EntityBuilder<'_> {
90    /// Resolve every named attribute to its STEP slot and produce the entity.
91    ///
92    /// # Errors
93    ///
94    /// Refuses an unknown entity type, an attribute the schema does not
95    /// declare, an attribute set twice, a required attribute left unset, a
96    /// scalar/aggregate confusion, a value whose shape contradicts the declared
97    /// type, and a malformed GlobalId.
98    pub fn build(self) -> AuthorResult<Entity> {
99        let declared = self.schema.attributes(&self.entity);
100        if declared.is_empty() && self.schema.entity(&self.entity).is_none() {
101            return Err(AuthorError::UnknownEntity {
102                entity: self.entity,
103            });
104        }
105
106        // Positional order comes from the schema: inherited attributes first,
107        // which is what makes a STEP record readable by anything else. A slot
108        // the schema derives for this entity is written `*` unless set.
109        let mut slots: Vec<Value> = declared
110            .iter()
111            .map(|attribute| {
112                if is_derived_slot(self.schema, &self.entity, &attribute.name) {
113                    Value::Derived
114                } else {
115                    Value::Null
116                }
117            })
118            .collect();
119        let mut filled = vec![false; declared.len()];
120
121        for (name, value) in &self.set {
122            let Some(index) = declared
123                .iter()
124                .position(|a| a.name.eq_ignore_ascii_case(name))
125            else {
126                return Err(AuthorError::UnknownAttribute {
127                    entity: self.entity.clone(),
128                    attribute: name.clone(),
129                    known: declared.iter().map(|a| a.name.clone()).collect(),
130                });
131            };
132            if filled[index] {
133                return Err(AuthorError::DuplicateAttribute {
134                    entity: self.entity.clone(),
135                    attribute: declared[index].name.clone(),
136                });
137            }
138            check_value(self.schema, &self.entity, declared[index], value)?;
139            slots[index] = value.clone();
140            filled[index] = true;
141        }
142
143        for (index, attribute) in declared.iter().enumerate() {
144            if !filled[index] && !attribute.optional && !matches!(slots[index], Value::Derived) {
145                return Err(AuthorError::MissingRequired {
146                    entity: self.entity.clone(),
147                    attribute: attribute.name.clone(),
148                });
149            }
150        }
151
152        Ok(Entity::new(self.entity.to_ascii_uppercase(), slots))
153    }
154
155    /// Build the entity and append it to `model`, returning its new id.
156    ///
157    /// The model is left untouched when construction fails, so a rejected
158    /// entity cannot leave a half-written record behind.
159    ///
160    /// # Errors
161    ///
162    /// Any failure from [`build`](EntityBuilder::build).
163    pub fn insert(self, model: &mut ifc_model::Model) -> AuthorResult<ifc_model::EntityId> {
164        let entity = self.build()?;
165        Ok(model.push(entity))
166    }
167}
168
169/// Check one value against one declared attribute.
170///
171/// Split out as a free function because it borrows the schema and the entity
172/// name while the builder's `set` list is being consumed.
173pub(crate) fn check_value(
174    schema: &Schema,
175    entity: &str,
176    attribute: &ifc_schema::Attribute,
177    value: &Value,
178) -> AuthorResult<()> {
179    // A derived slot admits exactly `*`, and `*` fits nowhere else.
180    let derived = is_derived_slot(schema, entity, &attribute.name);
181    match (derived, value) {
182        (true, Value::Derived) => return Ok(()),
183        (true, other) => {
184            return Err(AuthorError::DerivedAttribute {
185                entity: entity.to_owned(),
186                attribute: attribute.name.clone(),
187                found: describe_value(other),
188            })
189        }
190        (false, Value::Derived) => {
191            return Err(AuthorError::NotDerived {
192                entity: entity.to_owned(),
193                attribute: attribute.name.clone(),
194            })
195        }
196        (false, _) => {}
197    }
198
199    // An aggregate declaration wants a list and a scalar declaration does not.
200    // The `LIST` may sit in the attribute declaration or in a defined type the
201    // attribute is declared as (`IfcCompoundPlaneAngleMeasure`). `$` is exempt:
202    // an unset optional aggregate is still `$`, not `()`.
203    //
204    // An empty `()` is invalid for a `LIST [1:?]` / `SET [1:?]` attribute: a
205    // caller writes `$` when the attribute is optional and must not build the
206    // entity when it is required. That cannot be refused here, because the
207    // schema tables keep no aggregate bounds. TODO(#111)
208    let aliased = if attribute.aggregate {
209        None
210    } else {
211        aggregate_element(schema, &attribute.type_name)
212    };
213    let expected_aggregate = attribute.aggregate || aliased.is_some();
214    if !matches!(value, Value::Null) {
215        let supplied_aggregate = matches!(value, Value::List(_));
216        if supplied_aggregate != expected_aggregate {
217            return Err(AuthorError::AggregateMismatch {
218                entity: entity.to_owned(),
219                attribute: attribute.name.clone(),
220                expected_aggregate,
221            });
222        }
223    }
224
225    // GlobalId is the one attribute whose *content* is worth checking here: it
226    // is the only stable cross-file identity an element has, and a malformed
227    // one silently breaks diffing and issue tracking rather than failing loudly.
228    if attribute.name.eq_ignore_ascii_case("GlobalId") {
229        if let Value::Text(text) = value {
230            if ifc_model::guid::Guid::parse(text).is_none() {
231                return Err(AuthorError::InvalidGlobalId {
232                    entity: entity.to_owned(),
233                    found: text.to_string(),
234                });
235            }
236        }
237    }
238
239    // Aggregate element types are checked per item; the declaration names the
240    // element type, not the container. For an aliased aggregate the element
241    // type is the alias's, e.g. `INTEGER` for `IfcCompoundPlaneAngleMeasure`.
242    //
243    // The first refused item is the one reported; a wrong type is reported
244    // before a wrong form, since rewrapping would not fix it.
245    let element_type = aliased.as_deref().unwrap_or(&attribute.type_name);
246    let verdicts: Vec<(Verdict, &Value)> = match value {
247        Value::List(items) => items
248            .iter()
249            .map(|item| (judge_value(schema, element_type, item), item))
250            .collect(),
251        scalar => vec![(judge_value(schema, &attribute.type_name, scalar), scalar)],
252    };
253    if verdicts
254        .iter()
255        .any(|(verdict, _)| *verdict == Verdict::WrongType)
256    {
257        return Err(AuthorError::TypeMismatch {
258            entity: entity.to_owned(),
259            attribute: attribute.name.clone(),
260            expected: attribute.type_name.clone(),
261            found: describe_value(value),
262        });
263    }
264    if let Some((Verdict::WrongForm { typed_required }, offending)) = verdicts
265        .into_iter()
266        .find(|(verdict, _)| matches!(verdict, Verdict::WrongForm { .. }))
267    {
268        return Err(AuthorError::ValueForm {
269            entity: entity.to_owned(),
270            attribute: attribute.name.clone(),
271            declared: element_type.to_owned(),
272            typed_required,
273            found: describe_value(offending),
274        });
275    }
276    Ok(())
277}