Skip to main content

ifc_model/mutation/
edit.rs

1//! Schema-agnostic edit operations: update an existing entity's attributes
2//! or remove it, without corrupting the derived by-type index.
3//!
4//! # Why not a raw `&mut Entity`
5//!
6//! `Model.get_mut` cannot exist as a bare accessor: an entity's `type_name`
7//! is also a key into `Model::by_type`, so mutating the type field through a
8//! `&mut Entity` would silently desynchronize the index (`ids_of_type` keeps
9//! reporting the old type, or both). Every write therefore goes through this
10//! module, which knows how to keep the index correct.
11//!
12//! # What is NOT checked here
13//!
14//! These operations are schema-agnostic: they trust the caller with slot
15//! indices and don't know an entity's declared attribute count. Reference
16//! integrity, arity, and declared-type checks are `ifc-author`'s job on
17//! construction (`EntityBuilder`) and
18//! `ifc-validate`'s job on audit. This is the primitive both build on.
19
20use crate::entity::Entity;
21use crate::model::Model;
22use crate::value::{EntityId, Value};
23
24impl Model {
25    /// Set one positional attribute on an existing entity.
26    ///
27    /// Returns the value previously at that slot, or `None` if `id` does not
28    /// name an entity in this model. Growing past the entity's current
29    /// attribute count pads with [`Value::Null`], matching how STEP itself
30    /// treats a missing trailing optional attribute.
31    ///
32    /// Does not touch `type_name`, so the by-type index stays correct without
33    /// needing to run the same reindex logic [`Model::insert`] does. Renaming
34    /// an entity's type is a structural change, not an attribute edit: use
35    /// [`Model::retype`].
36    ///
37    /// ```
38    /// use ifc_model::{Entity, EntityId, Model, Value};
39    ///
40    /// let mut model = Model::new();
41    /// let id = model.push(Entity::new("IFCCARTESIANPOINT", vec![Value::List(vec![
42    ///     Value::Real(0.0), Value::Real(0.0),
43    /// ])]));
44    ///
45    /// let previous = model.set_attribute(id, 0, Value::List(vec![
46    ///     Value::Real(10.0), Value::Real(20.0),
47    /// ]));
48    /// assert!(previous.is_some());
49    /// let coords = model.get(id).unwrap().attribute(0).unwrap().as_list().unwrap();
50    /// assert_eq!(coords[0].as_f64(), Some(10.0));
51    /// ```
52    pub fn set_attribute(&mut self, id: EntityId, index: usize, value: Value) -> Option<Value> {
53        let entity = self.entity_mut(id)?;
54        if index >= entity.attributes.len() {
55            entity.attributes.resize(index + 1, Value::Null);
56        }
57        let previous = std::mem::replace(&mut entity.attributes[index], value);
58        self.bump_revision();
59        Some(previous)
60    }
61
62    /// Apply several attribute edits to one entity as a single unit.
63    ///
64    /// `edits` are applied in order; a later edit to the same slot wins. This
65    /// exists because a caller updating several attributes wants to do it in
66    /// one lookup rather than repeating [`Model::set_attribute`]'s entity
67    /// lookup per field — the practical difference for callers is that this
68    /// is one indexmap probe instead of N, not a transactional guarantee
69    /// (there is nothing to roll back: slot writes cannot themselves fail).
70    ///
71    /// Returns the previous values in `edits` order, or `None` if `id` does
72    /// not name an entity.
73    pub fn set_attributes(
74        &mut self,
75        id: EntityId,
76        edits: impl IntoIterator<Item = (usize, Value)>,
77    ) -> Option<Vec<Value>> {
78        let entity = self.entity_mut(id)?;
79        let mut previous = Vec::new();
80        for (index, value) in edits {
81            if index >= entity.attributes.len() {
82                entity.attributes.resize(index + 1, Value::Null);
83            }
84            previous.push(std::mem::replace(&mut entity.attributes[index], value));
85        }
86        self.bump_revision();
87        Some(previous)
88    }
89
90    /// Change an entity's type in place, keeping its id and attributes.
91    ///
92    /// Reindexes `by_type` so `ids_of_type` reflects the new type immediately
93    /// -- the operation [`Model::insert`] already performs when an id is
94    /// reused with a different type, exposed here without requiring the
95    /// caller to reconstruct the whole entity.
96    ///
97    /// Returns the previous type name, or `None` if `id` does not name an
98    /// entity.
99    pub fn retype(
100        &mut self,
101        id: EntityId,
102        type_name: impl Into<std::sync::Arc<str>>,
103    ) -> Option<std::sync::Arc<str>> {
104        let entity = self.entity_mut(id)?;
105        let previous = entity.type_name.clone();
106        let new_name = type_name.into();
107        #[cfg(feature = "authored-dump")]
108        crate::authored_dump::record(&new_name, "retype");
109        if previous.eq_ignore_ascii_case(&new_name) {
110            entity.type_name = new_name;
111            self.bump_revision();
112            return Some(previous);
113        }
114        entity.type_name = new_name.clone();
115
116        let old_key = previous.to_ascii_uppercase();
117        let new_key = new_name.to_ascii_uppercase();
118        if let Some(ids) = self.by_type_mut().get_mut(&old_key) {
119            ids.retain(|existing| *existing != id);
120        }
121        self.by_type_mut().entry(new_key).or_default().push(id);
122        self.bump_revision();
123
124        Some(previous)
125    }
126
127    /// Remove an entity entirely.
128    ///
129    /// Returns the removed entity, or `None` if `id` was not present. Leaves
130    /// every reference to `id` from other entities dangling -- detect those
131    /// with [`Model::dangling_references`] after a batch of removals, the
132    /// same way a codec would detect them in a hand-edited file.
133    pub fn remove(&mut self, id: EntityId) -> Option<Entity> {
134        let entity = self.take_entity(id)?;
135        let key = entity.type_name.to_ascii_uppercase();
136        if let Some(ids) = self.by_type_mut().get_mut(&key) {
137            ids.retain(|existing| *existing != id);
138        }
139        self.order_mut().retain(|existing| *existing != id);
140        self.bump_revision();
141        Some(entity)
142    }
143}