joydb/
relation.rs

1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2
3use crate::{JoydbError, Model};
4
5/// A relation is a collection of records of a particular model and some metadata.
6/// associated with the relation.
7///
8/// Relation also implements typical CRUD operations, which are used by the database.
9#[derive(Debug)]
10pub struct Relation<M: Model> {
11    /// Metadata for the relation.
12    /// This is not serialized or persisted. They meant to exist only in memory.
13    pub(crate) meta: RelationMeta,
14
15    /// The records in the relation.
16    pub(crate) records: Vec<M>,
17}
18
19impl<M> Default for Relation<M>
20where
21    M: Model,
22{
23    fn default() -> Self {
24        Self::new()
25    }
26}
27
28impl<M: Model> Relation<M> {
29    /// Creates a new empty relation.
30    pub(crate) fn new() -> Self {
31        Relation {
32            meta: RelationMeta::default(),
33            records: Vec::new(),
34        }
35    }
36
37    /// Creates a new relation with the given records.
38    ///
39    /// It needs to be public, since it may be used by custom partitioned adapters (.e.g
40    /// [crate::adapters::CsvAdapter] uses it).
41    pub fn new_with_records(records: Vec<M>) -> Self {
42        Relation {
43            meta: RelationMeta::default(),
44            records,
45        }
46    }
47
48    /// Is there any unsaved changes?
49    ///
50    /// It needs to be public, since it's invoked by the code generated with [crate::state] macro.
51    pub fn is_dirty(&self) -> bool {
52        self.meta.is_dirty
53    }
54
55    /// Resets the dirty flag to `false`.
56    ///
57    /// It needs to be public, since it's invoked by the code generated with [crate::state] macro.
58    pub fn reset_dirty(&mut self) {
59        self.meta.is_dirty = false;
60    }
61
62    /// Returns reference to the records.
63    /// This is intended to be used only by partitioned adapters.
64    pub fn records(&self) -> &[M] {
65        &self.records
66    }
67
68    pub(crate) fn insert(&mut self, record: &M) -> Result<(), JoydbError> {
69        let id = record.id();
70        let is_duplicated = self.records.iter().any(|m| m.id() == id);
71        if is_duplicated {
72            Err(JoydbError::DuplicatedId {
73                id: format!("{:?}", id),
74                model: M::model_name().to_owned(),
75            })
76        } else {
77            self.records.push(record.clone());
78            self.meta.is_dirty = true;
79            Ok(())
80        }
81    }
82
83    pub(crate) fn get(&self, id: &M::Id) -> Result<Option<M>, JoydbError> {
84        let maybe_record = self.records.iter().find(|m| m.id() == id).cloned();
85        Ok(maybe_record)
86    }
87
88    pub(crate) fn get_all(&self) -> Result<Vec<M>, JoydbError> {
89        Ok(self.records.to_vec())
90    }
91
92    /// Return all records that match the predicate.
93    pub(crate) fn get_all_by<F>(&self, predicate: F) -> Result<Vec<M>, JoydbError>
94    where
95        F: Fn(&M) -> bool,
96    {
97        let filtered_records = self
98            .records
99            .iter()
100            .filter(|m| predicate(m))
101            .cloned()
102            .collect();
103        Ok(filtered_records)
104    }
105
106    pub(crate) fn count(&self) -> Result<usize, JoydbError> {
107        Ok(self.records.len())
108    }
109
110    pub(crate) fn update(&mut self, new_record: &M) -> Result<(), JoydbError> {
111        let id = new_record.id();
112
113        if let Some(m) = self.records.iter_mut().find(|m| m.id() == id) {
114            *m = new_record.clone();
115            self.meta.is_dirty = true;
116            Ok(())
117        } else {
118            Err(JoydbError::NotFound {
119                id: format!("{:?}", id),
120                model: M::model_name().to_owned(),
121            })
122        }
123    }
124
125    pub(crate) fn upsert(&mut self, record: &M) -> Result<(), JoydbError> {
126        let target_id = record.id();
127        let maybe_target_record = self.records.iter_mut().find(|m| m.id() == target_id);
128        if let Some(target_record) = maybe_target_record {
129            *target_record = record.clone();
130        } else {
131            self.records.push(record.clone());
132        }
133        self.meta.is_dirty = true;
134        Ok(())
135    }
136
137    pub(crate) fn delete(&mut self, id: &M::Id) -> Result<Option<M>, JoydbError> {
138        let index = self.records.iter().position(|m| m.id() == id);
139        if let Some(index) = index {
140            let record = self.records.remove(index);
141            self.meta.is_dirty = true;
142            Ok(Some(record))
143        } else {
144            Ok(None)
145        }
146    }
147
148    pub(crate) fn delete_all_by<F>(&mut self, predicate: F) -> Result<Vec<M>, JoydbError>
149    where
150        F: Fn(&M) -> bool,
151    {
152        let mut deleted_records = Vec::new();
153        let mut retained_records = Vec::with_capacity(self.records.len());
154
155        for record in self.records.drain(..) {
156            if predicate(&record) {
157                deleted_records.push(record);
158                self.meta.is_dirty = true;
159            } else {
160                retained_records.push(record);
161            }
162        }
163        self.records = retained_records;
164
165        Ok(deleted_records)
166    }
167}
168
169/// Metadata for the relation.
170/// It's not serialized or persisted. They meant to exist only in memory.
171#[derive(Debug, Default)]
172pub struct RelationMeta {
173    pub(crate) is_dirty: bool,
174}
175
176// Custom serialization for Relation
177impl<M: Model> Serialize for Relation<M> {
178    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
179    where
180        S: Serializer,
181    {
182        self.records.serialize(serializer)
183    }
184}
185
186// Custom deserialization for Relation
187impl<'de, M: Model> Deserialize<'de> for Relation<M> {
188    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
189    where
190        D: Deserializer<'de>,
191    {
192        let models = Vec::<M>::deserialize(deserializer)?;
193        Ok(Relation {
194            meta: RelationMeta::default(),
195            records: models,
196        })
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203    use serde::{Deserialize, Serialize};
204
205    #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
206    struct Post {
207        id: u32,
208        title: String,
209    }
210
211    impl Model for Post {
212        type Id = u32;
213
214        fn id(&self) -> &Self::Id {
215            &self.id
216        }
217
218        fn model_name() -> &'static str {
219            "Post"
220        }
221    }
222
223    fn sample_posts() -> Vec<Post> {
224        vec![first_post(), second_post(), third_post()]
225    }
226
227    fn first_post() -> Post {
228        Post {
229            id: 1,
230            title: "First".to_string(),
231        }
232    }
233
234    fn second_post() -> Post {
235        Post {
236            id: 2,
237            title: "Second".to_string(),
238        }
239    }
240
241    fn third_post() -> Post {
242        Post {
243            id: 3,
244            title: "Third".to_string(),
245        }
246    }
247
248    fn sample_relation() -> Relation<Post> {
249        Relation {
250            meta: RelationMeta { is_dirty: false },
251            records: sample_posts(),
252        }
253    }
254
255    mod serialization_and_deserialization {
256        use super::*;
257
258        #[test]
259        fn test_serialize_relation() {
260            let relation = Relation {
261                meta: RelationMeta { is_dirty: false },
262                records: sample_posts(),
263            };
264
265            let json = serde_json::to_string(&relation).unwrap();
266            assert_eq!(
267                json,
268                r#"[{"id":1,"title":"First"},{"id":2,"title":"Second"},{"id":3,"title":"Third"}]"#
269            );
270        }
271
272        #[test]
273        fn test_deserialize_relation() {
274            let json = r#"[{"id":10,"title":"One"},{"id":20,"title":"Two"}]"#;
275
276            let relation: Relation<Post> = serde_json::from_str(json).unwrap();
277
278            assert_eq!(relation.records.len(), 2);
279            assert_eq!(relation.records[0].id, 10);
280            assert_eq!(relation.records[0].title, "One");
281            assert_eq!(relation.records[1].id, 20);
282            assert_eq!(relation.records[1].title, "Two");
283
284            // The meta field should be default-initialized
285            assert_eq!(relation.meta.is_dirty, false);
286        }
287
288        #[test]
289        fn test_serialize_deserialize_roundtrip() {
290            let original = Relation {
291                meta: RelationMeta { is_dirty: true },
292                records: sample_posts(),
293            };
294
295            let json = serde_json::to_string(&original).unwrap();
296            let deserialized: Relation<Post> = serde_json::from_str(&json).unwrap();
297
298            assert_eq!(original.records, deserialized.records);
299            assert_eq!(deserialized.meta.is_dirty, false); // Meta is not serialized
300        }
301    }
302
303    mod insert {
304        use super::*;
305
306        #[test]
307        fn should_insert_new_record_and_mark_dirty() {
308            let mut relation = sample_relation();
309            assert_eq!(relation.records.len(), 3);
310            assert_eq!(relation.meta.is_dirty, false);
311
312            let post = Post {
313                id: 13,
314                title: "Thirteen".to_string(),
315            };
316            relation.insert(&post).unwrap();
317
318            assert_eq!(relation.records.len(), 4);
319            assert_eq!(relation.records[3], post);
320            assert_eq!(relation.meta.is_dirty, true);
321        }
322
323        #[test]
324        fn should_return_an_error_when_record_with_id_already_exists() {
325            let mut relation = Relation::new();
326            let post = Post {
327                id: 777,
328                title: "First".to_string(),
329            };
330            relation.insert(&post).unwrap();
331
332            let another_post = Post {
333                id: 777,
334                title: "Another First".to_string(),
335            };
336            let err = relation.insert(&another_post).unwrap_err();
337
338            assert!(matches!(err, JoydbError::DuplicatedId { .. }));
339            assert_eq!(
340                err.to_string(),
341                format!("Post with id = 777 already exists")
342            );
343        }
344    }
345
346    mod get {
347        use super::*;
348
349        #[test]
350        fn should_return_none_when_record_not_found() {
351            let relation = sample_relation();
352            let id = 999;
353            let maybe_post = relation.get(&id).unwrap();
354            assert!(maybe_post.is_none());
355        }
356
357        #[test]
358        fn should_return_record_when_found() {
359            let relation = sample_relation();
360            let id = 2;
361            let maybe_post = relation.get(&id).unwrap();
362            let post = maybe_post.unwrap();
363            assert_eq!(post, second_post());
364        }
365    }
366
367    mod get_all {
368        use super::*;
369
370        #[test]
371        fn should_return_all_records() {
372            let relation = sample_relation();
373            let all_posts = relation.get_all().unwrap();
374            assert_eq!(all_posts, sample_posts());
375        }
376    }
377
378    mod count {
379        use super::*;
380
381        #[test]
382        fn should_return_number_of_records() {
383            let relation = sample_relation();
384            let count = relation.count().unwrap();
385            assert_eq!(count, 3);
386        }
387    }
388
389    mod update {
390        use super::*;
391
392        #[test]
393        fn should_update_record_and_mark_dirty() {
394            let mut relation = sample_relation();
395            let new_post = Post {
396                id: 2,
397                title: "Updated Second".to_string(),
398            };
399            relation.update(&new_post).unwrap();
400
401            let updated_post = relation.get(&2).unwrap().unwrap();
402            assert_eq!(updated_post, new_post);
403            assert_eq!(relation.meta.is_dirty, true);
404        }
405
406        #[test]
407        fn should_return_error_when_record_not_found() {
408            let mut relation = sample_relation();
409            let new_post = Post {
410                id: 999,
411                title: "Updated Second".to_string(),
412            };
413            let err = relation.update(&new_post).unwrap_err();
414
415            assert!(matches!(err, JoydbError::NotFound { .. }));
416            assert_eq!(err.to_string(), format!("Post with id = 999 not found"));
417        }
418    }
419
420    mod delete {
421        use super::*;
422
423        #[test]
424        fn should_delete_record_and_mark_dirty() {
425            let mut relation = sample_relation();
426            let id = 1;
427            let deleted_post = relation.delete(&id).unwrap().unwrap();
428
429            assert_eq!(relation.records.len(), 2);
430            assert_eq!(relation.records[0], second_post());
431            assert_eq!(relation.meta.is_dirty, true);
432            assert_eq!(deleted_post, first_post());
433        }
434
435        #[test]
436        fn should_return_none_when_record_not_found() {
437            let mut relation = sample_relation();
438            let id = 555;
439            let maybe_post = relation.delete(&id).unwrap();
440            assert!(maybe_post.is_none());
441            assert_eq!(relation.records.len(), 3);
442            assert_eq!(relation.meta.is_dirty, false);
443        }
444    }
445
446    mod delete_all_by {
447        use super::*;
448
449        #[test]
450        fn should_delete_all_records_that_match_predicate() {
451            let mut relation = sample_relation();
452            assert_eq!(relation.records.len(), 3);
453
454            let deleted_records = relation.delete_all_by(|post: &Post| post.id >= 2).unwrap();
455
456            assert_eq!(deleted_records.len(), 2);
457            assert!(deleted_records.contains(&second_post()));
458            assert!(deleted_records.contains(&third_post()));
459
460            assert_eq!(relation.records.len(), 1);
461            assert_eq!(relation.records[0], first_post());
462            assert_eq!(relation.meta.is_dirty, true);
463        }
464
465        #[test]
466        fn should_not_delete_anything_if_no_record_matches_predicate() {
467            let mut relation = sample_relation();
468            assert_eq!(relation.records.len(), 3);
469
470            let deleted_records = relation.delete_all_by(|post: &Post| post.id > 777).unwrap();
471
472            assert_eq!(deleted_records.len(), 0);
473
474            assert_eq!(relation.records.len(), 3);
475            assert_eq!(relation.meta.is_dirty, false);
476        }
477    }
478
479    mod upsert {
480        use super::*;
481
482        #[test]
483        fn should_add_new_record_if_does_not_exist_yet() {
484            let mut relation = sample_relation();
485            assert_eq!(relation.records.len(), 3);
486
487            let post44 = Post {
488                id: 44,
489                title: "Forty Four".to_string(),
490            };
491            relation.upsert(&post44).unwrap();
492
493            assert_eq!(relation.records.len(), 4);
494            assert_eq!(relation.records[3], post44);
495        }
496
497        #[test]
498        fn should_update_existing_record_matched_by_id() {
499            let mut relation = sample_relation();
500            assert_eq!(relation.records.len(), 3);
501
502            let updated_post2 = Post {
503                id: 2,
504                title: "Updated Second!!!".to_string(),
505            };
506
507            relation.upsert(&updated_post2).unwrap();
508
509            assert_eq!(relation.records.len(), 3);
510            assert_eq!(relation.get(&2).unwrap(), Some(updated_post2));
511        }
512    }
513
514    #[test]
515    fn should_reset_dirty() {
516        let mut relation = sample_relation();
517        assert_eq!(relation.is_dirty(), false);
518
519        relation.delete(&1).unwrap();
520        assert_eq!(relation.is_dirty(), true);
521
522        relation.reset_dirty();
523        assert_eq!(relation.is_dirty(), false);
524    }
525}