Skip to main content

ifc_author/editor/
entity.rs

1//! Schema-checked updates of existing IFC entities.
2//!
3//! A [`Transaction`] cannot be read back: staged edits are invisible to
4//! `Model::get` until the transaction commits. To assert on a staged entity,
5//! commit first or inspect [`Transaction::edits`].
6
7use ifc_model::{Entity, EntityId, Model, Transaction, Value};
8use ifc_schema::Schema;
9
10use crate::builder::check_value;
11use crate::error::{AuthorError, AuthorResult};
12
13/// A schema-checked edit of one entity already present in a [`Model`].
14///
15/// Named edits are resolved against the entity's declaration and the entire
16/// projected entity is validated before any write is staged. Reference
17/// integrity, stale revisions, and atomic commit remain [`Transaction`]'s
18/// responsibility.
19#[derive(Debug, Clone)]
20pub struct EntityEditor<'a> {
21    schema: &'a Schema,
22    id: EntityId,
23    entity: Entity,
24    set: Vec<(String, Value)>,
25}
26
27impl<'a> EntityEditor<'a> {
28    /// Start editing `id` against the model snapshot used to open a transaction.
29    ///
30    /// # Errors
31    ///
32    /// Returns [`AuthorError::MissingEntity`] when `id` is absent. A caller
33    /// should create its [`Transaction`] from the same model snapshot.
34    pub fn new(schema: &'a Schema, model: &Model, id: EntityId) -> AuthorResult<Self> {
35        let entity = model
36            .get(id)
37            .cloned()
38            .ok_or(AuthorError::MissingEntity { id })?;
39        Ok(Self {
40            schema,
41            id,
42            entity,
43            set: Vec::new(),
44        })
45    }
46
47    /// Replace one attribute by its declared name.
48    #[must_use]
49    pub fn set(mut self, attribute: impl Into<String>, value: Value) -> Self {
50        self.set.push((attribute.into(), value));
51        self
52    }
53
54    /// Replace a text attribute.
55    #[must_use]
56    pub fn text(self, attribute: impl Into<String>, text: impl Into<std::sync::Arc<str>>) -> Self {
57        self.set(attribute, Value::Text(text.into()))
58    }
59
60    /// Replace a real-valued attribute.
61    #[must_use]
62    pub fn real(self, attribute: impl Into<String>, value: f64) -> Self {
63        self.set(attribute, Value::Real(value))
64    }
65
66    /// Replace an entity-reference attribute.
67    #[must_use]
68    pub fn reference(self, attribute: impl Into<String>, id: EntityId) -> Self {
69        self.set(attribute, Value::Ref(id))
70    }
71
72    /// Replace an enumeration attribute.
73    #[must_use]
74    pub fn enumeration(
75        self,
76        attribute: impl Into<String>,
77        constant: impl Into<std::sync::Arc<str>>,
78    ) -> Self {
79        self.set(attribute, Value::Enum(constant.into()))
80    }
81
82    /// Validate the projected entity and stage its changed slots.
83    ///
84    /// Validation completes before the first call to [`Transaction::set_attribute`],
85    /// so any [`AuthorError`] leaves `transaction` unchanged.
86    pub fn stage(self, transaction: &mut Transaction) -> AuthorResult<()> {
87        let entity_name = self.entity.type_name.to_string();
88        let declared = self.schema.attributes(&entity_name);
89        if declared.is_empty() && self.schema.entity(&entity_name).is_none() {
90            return Err(AuthorError::UnknownEntity {
91                entity: entity_name,
92            });
93        }
94        if self.entity.attributes.len() != declared.len() {
95            return Err(AuthorError::ArityMismatch {
96                entity: entity_name,
97                expected: declared.len(),
98                found: self.entity.attributes.len(),
99            });
100        }
101
102        let mut projected = self.entity.attributes.clone();
103        let mut edited = vec![false; declared.len()];
104        let mut staged = Vec::with_capacity(self.set.len());
105        for (name, value) in self.set {
106            let Some(index) = declared
107                .iter()
108                .position(|attribute| attribute.name.eq_ignore_ascii_case(&name))
109            else {
110                return Err(AuthorError::UnknownAttribute {
111                    entity: entity_name,
112                    attribute: name,
113                    known: declared
114                        .iter()
115                        .map(|attribute| attribute.name.clone())
116                        .collect(),
117                });
118            };
119            if edited[index] {
120                return Err(AuthorError::DuplicateAttribute {
121                    entity: entity_name,
122                    attribute: declared[index].name.clone(),
123                });
124            }
125            edited[index] = true;
126            projected[index] = value.clone();
127            staged.push((index, value));
128        }
129
130        for (index, attribute) in declared.iter().enumerate() {
131            let value = &projected[index];
132            // Checked first so a derived slot holding `$` reports as derived,
133            // not as a missing required value.
134            check_value(self.schema, &entity_name, attribute, value)?;
135            if !attribute.optional && matches!(value, Value::Null) {
136                return Err(AuthorError::MissingRequired {
137                    entity: entity_name,
138                    attribute: attribute.name.clone(),
139                });
140            }
141        }
142
143        for (slot, value) in staged {
144            if self.entity.attributes[slot] != value {
145                transaction.set_attribute(self.id, slot, value);
146            }
147        }
148        Ok(())
149    }
150}