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