Skip to main content

teaql_runtime/
entity_status.rs

1/// Actions that can be applied to an entity to transition its status.
2#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
3pub enum EntityAction {
4    Update,
5    Delete,
6    Persist,
7    Recover,
8}
9
10/// Tracks the lifecycle status of an entity through a state machine.
11///
12/// State transitions follow the Java TeaQL transition table exactly:
13///
14/// | Current State     | Action  | Next State        |
15/// |-------------------|---------|-------------------|
16/// | New               | Update  | New               |
17/// | New               | Persist | Persisted         |
18/// | Persisted         | Update  | Updated           |
19/// | Persisted         | Delete  | UpdatedDeleted    |
20/// | PersistedDeleted  | Recover | UpdatedRecover    |
21/// | Updated           | Update  | Updated           |
22/// | Updated           | Persist | Persisted         |
23/// | UpdatedDeleted    | Persist | PersistedDeleted  |
24/// | UpdatedDeleted    | Delete  | UpdatedDeleted    |
25/// | UpdatedRecover    | Persist | Persisted         |
26/// | UpdatedRecover    | Recover | UpdatedRecover    |
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
28pub enum EntityStatus {
29    /// A newly created entity that has not been persisted yet.
30    #[default]
31    New,
32    /// An entity that has been persisted to the database.
33    Persisted,
34    /// A persisted entity that has been soft-deleted.
35    PersistedDeleted,
36    /// A persisted entity with pending updates.
37    Updated,
38    /// A persisted entity that has been updated and then deleted.
39    UpdatedDeleted,
40    /// A deleted entity that has been recovered (undeleted).
41    UpdatedRecover,
42    /// A reference to an entity managed elsewhere.
43    Refer,
44}
45
46impl EntityStatus {
47    /// Transition to the next status given an action.
48    ///
49    /// Returns `Err` with a descriptive message for invalid transitions.
50    pub fn next(self, action: EntityAction) -> Result<EntityStatus, String> {
51        match (self, action) {
52            // New
53            (EntityStatus::New, EntityAction::Update) => Ok(EntityStatus::New),
54            (EntityStatus::New, EntityAction::Persist) => Ok(EntityStatus::Persisted),
55
56            // Persisted
57            (EntityStatus::Persisted, EntityAction::Update) => Ok(EntityStatus::Updated),
58            (EntityStatus::Persisted, EntityAction::Delete) => Ok(EntityStatus::UpdatedDeleted),
59
60            // PersistedDeleted
61            (EntityStatus::PersistedDeleted, EntityAction::Recover) => {
62                Ok(EntityStatus::UpdatedRecover)
63            }
64
65            // Updated
66            (EntityStatus::Updated, EntityAction::Update) => Ok(EntityStatus::Updated),
67            (EntityStatus::Updated, EntityAction::Persist) => Ok(EntityStatus::Persisted),
68
69            // UpdatedDeleted
70            (EntityStatus::UpdatedDeleted, EntityAction::Persist) => {
71                Ok(EntityStatus::PersistedDeleted)
72            }
73            (EntityStatus::UpdatedDeleted, EntityAction::Delete) => {
74                Ok(EntityStatus::UpdatedDeleted)
75            }
76
77            // UpdatedRecover
78            (EntityStatus::UpdatedRecover, EntityAction::Persist) => Ok(EntityStatus::Persisted),
79            (EntityStatus::UpdatedRecover, EntityAction::Recover) => {
80                Ok(EntityStatus::UpdatedRecover)
81            }
82
83            // All other combinations are invalid
84            (status, action) => Err(format!(
85                "invalid entity status transition: {:?} + {:?}",
86                status, action
87            )),
88        }
89    }
90
91    /// Returns `true` if this entity needs to be persisted (i.e. it has pending changes).
92    ///
93    /// Entities with status `New`, `Updated`, `UpdatedDeleted`, or `UpdatedRecover`
94    /// need persistence. `Persisted`, `PersistedDeleted`, and `Refer` do not.
95    pub fn need_persist(&self) -> bool {
96        matches!(
97            self,
98            EntityStatus::New
99                | EntityStatus::Updated
100                | EntityStatus::UpdatedDeleted
101                | EntityStatus::UpdatedRecover
102        )
103    }
104
105    /// Returns `true` if this entity is newly created and has never been persisted.
106    pub fn is_new(&self) -> bool {
107        matches!(self, EntityStatus::New)
108    }
109
110    /// Returns `true` if this entity has been updated since last persistence.
111    pub fn is_updated(&self) -> bool {
112        matches!(self, EntityStatus::Updated)
113    }
114
115    /// Returns `true` if this entity is marked for deletion.
116    pub fn is_deleted(&self) -> bool {
117        matches!(
118            self,
119            EntityStatus::UpdatedDeleted | EntityStatus::PersistedDeleted
120        )
121    }
122
123    /// Returns `true` if this entity has been recovered from deletion.
124    pub fn is_recover(&self) -> bool {
125        matches!(self, EntityStatus::UpdatedRecover)
126    }
127}
128
129impl std::fmt::Display for EntityStatus {
130    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131        match self {
132            EntityStatus::New => write!(f, "New"),
133            EntityStatus::Persisted => write!(f, "Persisted"),
134            EntityStatus::PersistedDeleted => write!(f, "PersistedDeleted"),
135            EntityStatus::Updated => write!(f, "Updated"),
136            EntityStatus::UpdatedDeleted => write!(f, "UpdatedDeleted"),
137            EntityStatus::UpdatedRecover => write!(f, "UpdatedRecover"),
138            EntityStatus::Refer => write!(f, "Refer"),
139        }
140    }
141}
142
143impl std::fmt::Display for EntityAction {
144    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145        match self {
146            EntityAction::Update => write!(f, "Update"),
147            EntityAction::Delete => write!(f, "Delete"),
148            EntityAction::Persist => write!(f, "Persist"),
149            EntityAction::Recover => write!(f, "Recover"),
150        }
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn default_is_new() {
160        assert_eq!(EntityStatus::default(), EntityStatus::New);
161    }
162
163    #[test]
164    fn new_update_stays_new() {
165        assert_eq!(
166            EntityStatus::New.next(EntityAction::Update).unwrap(),
167            EntityStatus::New
168        );
169    }
170
171    #[test]
172    fn new_persist_becomes_persisted() {
173        assert_eq!(
174            EntityStatus::New.next(EntityAction::Persist).unwrap(),
175            EntityStatus::Persisted
176        );
177    }
178
179    #[test]
180    fn persisted_update_becomes_updated() {
181        assert_eq!(
182            EntityStatus::Persisted.next(EntityAction::Update).unwrap(),
183            EntityStatus::Updated
184        );
185    }
186
187    #[test]
188    fn persisted_delete_becomes_updated_deleted() {
189        assert_eq!(
190            EntityStatus::Persisted.next(EntityAction::Delete).unwrap(),
191            EntityStatus::UpdatedDeleted
192        );
193    }
194
195    #[test]
196    fn persisted_deleted_recover_becomes_updated_recover() {
197        assert_eq!(
198            EntityStatus::PersistedDeleted
199                .next(EntityAction::Recover)
200                .unwrap(),
201            EntityStatus::UpdatedRecover
202        );
203    }
204
205    #[test]
206    fn updated_update_stays_updated() {
207        assert_eq!(
208            EntityStatus::Updated.next(EntityAction::Update).unwrap(),
209            EntityStatus::Updated
210        );
211    }
212
213    #[test]
214    fn updated_persist_becomes_persisted() {
215        assert_eq!(
216            EntityStatus::Updated.next(EntityAction::Persist).unwrap(),
217            EntityStatus::Persisted
218        );
219    }
220
221    #[test]
222    fn updated_deleted_persist_becomes_persisted_deleted() {
223        assert_eq!(
224            EntityStatus::UpdatedDeleted
225                .next(EntityAction::Persist)
226                .unwrap(),
227            EntityStatus::PersistedDeleted
228        );
229    }
230
231    #[test]
232    fn updated_deleted_delete_stays_updated_deleted() {
233        assert_eq!(
234            EntityStatus::UpdatedDeleted
235                .next(EntityAction::Delete)
236                .unwrap(),
237            EntityStatus::UpdatedDeleted
238        );
239    }
240
241    #[test]
242    fn updated_recover_persist_becomes_persisted() {
243        assert_eq!(
244            EntityStatus::UpdatedRecover
245                .next(EntityAction::Persist)
246                .unwrap(),
247            EntityStatus::Persisted
248        );
249    }
250
251    #[test]
252    fn updated_recover_recover_stays_updated_recover() {
253        assert_eq!(
254            EntityStatus::UpdatedRecover
255                .next(EntityAction::Recover)
256                .unwrap(),
257            EntityStatus::UpdatedRecover
258        );
259    }
260
261    #[test]
262    fn invalid_transition_returns_err() {
263        assert!(EntityStatus::New.next(EntityAction::Delete).is_err());
264        assert!(EntityStatus::New.next(EntityAction::Recover).is_err());
265        assert!(EntityStatus::Persisted.next(EntityAction::Persist).is_err());
266        assert!(EntityStatus::Persisted.next(EntityAction::Recover).is_err());
267        assert!(
268            EntityStatus::PersistedDeleted
269                .next(EntityAction::Update)
270                .is_err()
271        );
272        assert!(
273            EntityStatus::PersistedDeleted
274                .next(EntityAction::Delete)
275                .is_err()
276        );
277        assert!(
278            EntityStatus::PersistedDeleted
279                .next(EntityAction::Persist)
280                .is_err()
281        );
282        assert!(EntityStatus::Updated.next(EntityAction::Delete).is_err());
283        assert!(EntityStatus::Updated.next(EntityAction::Recover).is_err());
284        assert!(
285            EntityStatus::UpdatedDeleted
286                .next(EntityAction::Update)
287                .is_err()
288        );
289        assert!(
290            EntityStatus::UpdatedDeleted
291                .next(EntityAction::Recover)
292                .is_err()
293        );
294        assert!(
295            EntityStatus::UpdatedRecover
296                .next(EntityAction::Update)
297                .is_err()
298        );
299        assert!(
300            EntityStatus::UpdatedRecover
301                .next(EntityAction::Delete)
302                .is_err()
303        );
304        assert!(EntityStatus::Refer.next(EntityAction::Update).is_err());
305        assert!(EntityStatus::Refer.next(EntityAction::Delete).is_err());
306        assert!(EntityStatus::Refer.next(EntityAction::Persist).is_err());
307        assert!(EntityStatus::Refer.next(EntityAction::Recover).is_err());
308    }
309
310    #[test]
311    fn need_persist_flags() {
312        assert!(EntityStatus::New.need_persist());
313        assert!(!EntityStatus::Persisted.need_persist());
314        assert!(!EntityStatus::PersistedDeleted.need_persist());
315        assert!(EntityStatus::Updated.need_persist());
316        assert!(EntityStatus::UpdatedDeleted.need_persist());
317        assert!(EntityStatus::UpdatedRecover.need_persist());
318        assert!(!EntityStatus::Refer.need_persist());
319    }
320
321    #[test]
322    fn helper_predicates() {
323        assert!(EntityStatus::New.is_new());
324        assert!(!EntityStatus::Persisted.is_new());
325
326        assert!(EntityStatus::Updated.is_updated());
327        assert!(!EntityStatus::New.is_updated());
328
329        assert!(EntityStatus::UpdatedDeleted.is_deleted());
330        assert!(EntityStatus::PersistedDeleted.is_deleted());
331        assert!(!EntityStatus::Updated.is_deleted());
332
333        assert!(EntityStatus::UpdatedRecover.is_recover());
334        assert!(!EntityStatus::Updated.is_recover());
335    }
336
337    #[test]
338    fn full_lifecycle_create_update_delete() {
339        let status = EntityStatus::default();
340        assert_eq!(status, EntityStatus::New);
341
342        let status = status.next(EntityAction::Persist).unwrap();
343        assert_eq!(status, EntityStatus::Persisted);
344
345        let status = status.next(EntityAction::Update).unwrap();
346        assert_eq!(status, EntityStatus::Updated);
347
348        let status = status.next(EntityAction::Persist).unwrap();
349        assert_eq!(status, EntityStatus::Persisted);
350
351        let status = status.next(EntityAction::Delete).unwrap();
352        assert_eq!(status, EntityStatus::UpdatedDeleted);
353
354        let status = status.next(EntityAction::Persist).unwrap();
355        assert_eq!(status, EntityStatus::PersistedDeleted);
356
357        let status = status.next(EntityAction::Recover).unwrap();
358        assert_eq!(status, EntityStatus::UpdatedRecover);
359
360        let status = status.next(EntityAction::Persist).unwrap();
361        assert_eq!(status, EntityStatus::Persisted);
362    }
363}