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}