Skip to main content

open_timeline_core/
entity.rs

1// SPDX-License-Identifier: MIT
2
3//!
4//! The OpenTimeline entity type
5//!
6
7use crate::{Date, Day, HasIdAndName, Month, Name, OpenTimelineId, Year};
8use bool_tag_expr::{BoolTagExpr, Tag, Tags};
9use serde::{Deserialize, Deserializer, Serialize};
10use std::cmp::Ordering;
11use thiserror::Error;
12
13// TODO: improve (add more fine grain variants)?
14/// Errors that can arise in relation to an [`Entity`]
15#[derive(Error, Debug)]
16pub enum EntityError {
17    #[error("The entity dates are invalid")]
18    Dates,
19}
20
21/// The OpenTimeline [`Entity`] type
22#[derive(Serialize, Clone, Debug, PartialEq, Eq, Hash)]
23pub struct Entity {
24    /// The entity's ID
25    id: Option<OpenTimelineId>,
26
27    /// The entity's name
28    name: Name,
29
30    /// When did the entity begin/start
31    start: Date,
32
33    /// When did the entity end/finish (if it has)
34    end: Option<Date>,
35
36    /// Tags for the entity
37    tags: Option<Tags>,
38}
39
40// TODO: write a derive macro to derive Ord only from the ID for use with
41// all types?
42// Ord using just the Entity ID (Date does not, and can not, have a full ordering)
43impl Ord for Entity {
44    fn cmp(&self, other: &Self) -> Ordering {
45        self.id.cmp(&other.id)
46    }
47}
48
49// Just use the full Ord
50impl PartialOrd for Entity {
51    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
52        Some(self.cmp(other))
53    }
54}
55
56impl Entity {
57    /// Create a valid OpenTimeline [`Entity`] if it is possible to do so with
58    /// the values passed in
59    pub fn from(
60        id: Option<OpenTimelineId>,
61        name: Name,
62        start: Date,
63        end: Option<Date>,
64        tags: Option<Tags>,
65    ) -> Result<Entity, EntityError> {
66        let entity = Entity {
67            id,
68            name,
69            start,
70            end,
71            tags,
72        };
73
74        if entity.has_valid_dates() {
75            Ok(entity)
76        } else {
77            Err(EntityError::Dates)
78        }
79    }
80
81    /// Clear the [`Entity`]'s ID
82    pub fn clear_id(&mut self) {
83        self.id = None;
84    }
85
86    /// Whether the entity has valid dates
87    fn has_valid_dates(&self) -> bool {
88        if let Some(end) = &self.end {
89            if end < &self.start {
90                return false;
91            }
92        }
93        true
94    }
95
96    /// Borrow the entity's [`Tags`]
97    pub fn tags(&self) -> &Option<Tags> {
98        &self.tags
99    }
100
101    /// Mutably borrow the entity's [`Tags`]
102    pub fn tags_mut(&mut self) -> &mut Option<Tags> {
103        &mut self.tags
104    }
105
106    /// Add a tag to the entity
107    pub fn add_tag(&mut self, tag: Tag) {
108        self.tags.get_or_insert_with(Tags::new).insert(tag);
109    }
110
111    /// Remove a tag from the entity
112    pub fn remove_tag(&mut self, tag: &Tag) {
113        if let Some(tags) = self.tags.as_mut() {
114            tags.remove(tag);
115            if tags.is_empty() {
116                self.tags = None
117            }
118        }
119    }
120
121    /// Set the entity's [`Tags`]
122    pub fn set_tags(&mut self, tags: Tags) {
123        self.tags = (!tags.is_empty()).then_some(tags);
124    }
125
126    /// Clear the entity's [`Tags`] and set to `None`
127    pub fn clear_tags(&mut self) {
128        self.tags = None;
129    }
130
131    /// Get the entity's start [`Date`]
132    pub fn start(&self) -> Date {
133        self.start
134    }
135
136    /// Set the entity's start [`Date`] if it'll be valid
137    pub fn set_start(&mut self, start: Date) -> Result<(), EntityError> {
138        let mut tmp_entity = self.clone();
139        tmp_entity.start = start;
140        if !tmp_entity.has_valid_dates() {
141            return Err(EntityError::Dates);
142        }
143        self.start = start;
144        Ok(())
145    }
146
147    /// Get the entity's end [`Date`]
148    pub fn end(&self) -> Option<Date> {
149        self.end
150    }
151
152    /// Set the entity's end [`Date`] if it'll be valid
153    pub fn set_end(&mut self, end: Date) -> Result<(), EntityError> {
154        let mut tmp_entity = self.clone();
155        tmp_entity.end = Some(end);
156        if !tmp_entity.has_valid_dates() {
157            return Err(EntityError::Dates);
158        }
159        self.end = Some(end);
160        Ok(())
161    }
162
163    /// Check if the entity's end year is set
164    pub fn end_year_is_set(&self) -> bool {
165        self.end_year().is_some()
166    }
167
168    /// Check if the entity's end year is set
169    pub fn end_year(&self) -> Option<Year> {
170        self.end.map(|date| date.year())
171    }
172
173    /// Check if the entity's end month is set
174    pub fn end_month(&self) -> Option<Month> {
175        self.end.and_then(|date| date.month())
176    }
177
178    /// Check if the entity's end day is set
179    pub fn end_day(&self) -> Option<Day> {
180        self.end.and_then(|date| date.day())
181    }
182
183    /// Check if the entity's start year is set
184    pub fn start_year(&self) -> Year {
185        self.start.year()
186    }
187
188    /// Check if the entity's start month is set
189    pub fn start_month(&self) -> Option<Month> {
190        self.start.month()
191    }
192
193    /// Check if the entity's start day is set
194    pub fn start_day(&self) -> Option<Day> {
195        self.start.day()
196    }
197
198    /// Whether the entity in question matches the boolean tag expression.  This
199    /// can be used to filter a list of entities by a boolean tag expression.
200    pub fn matches_bool_tag_expr(&self, bool_tag_expr: &BoolTagExpr) -> bool {
201        let Some(tags) = self.tags() else {
202            return false;
203        };
204        bool_tag_expr.matches(tags)
205    }
206}
207
208impl HasIdAndName for Entity {
209    fn id(&self) -> Option<OpenTimelineId> {
210        self.id
211    }
212
213    fn set_id(&mut self, id: OpenTimelineId) {
214        self.id = Some(id)
215    }
216
217    fn name(&self) -> &Name {
218        &self.name
219    }
220
221    fn set_name(&mut self, name: Name) {
222        self.name = name
223    }
224}
225
226/// Used only by the custom deserialiser (to make it simpler)
227#[derive(Deserialize, Debug)]
228pub struct RawEndDate {
229    day: Option<i64>,
230    month: Option<i64>,
231    year: Option<i64>,
232}
233
234/// Used only by the custom deserialiser (to make it simpler)
235#[derive(Deserialize, Debug)]
236struct RawEntity {
237    id: Option<OpenTimelineId>,
238    name: Name,
239    start: Date,
240    end: Option<RawEndDate>,
241    tags: Option<Tags>,
242}
243
244impl<'de> Deserialize<'de> for Entity {
245    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
246    where
247        D: Deserializer<'de>,
248    {
249        // TODO: look into serde Visitors (and do without RawEntity)
250        let raw_entity = RawEntity::deserialize(deserializer)?;
251
252        // Deal with the incoming JSON having:
253        // "end":{"year":null,"month":null,"day":null}
254        // ie end isn't null but should be
255        let end = match raw_entity.end {
256            None => None,
257            Some(end) => {
258                if end.day.is_none() && end.month.is_none() && end.year.is_none() {
259                    None
260                } else if end.year.is_none() {
261                    // i.e. year is None, but day OR month are Some
262                    let err_msg = String::from(
263                        "End year is invalid (day and/or month is set, but year isn't",
264                    );
265                    return Err(serde::de::Error::custom(err_msg));
266                } else {
267                    match Date::from(end.day, end.month, end.year.unwrap()) {
268                        Ok(end) => Some(end),
269                        Err(_) => {
270                            // TODO: improve
271                            let err_msg = String::from("End year is invalid");
272                            return Err(serde::de::Error::custom(err_msg));
273                        }
274                    }
275                }
276            }
277        };
278
279        Entity::from(
280            raw_entity.id,
281            raw_entity.name,
282            raw_entity.start,
283            end,
284            raw_entity.tags,
285        )
286        .map_err(serde::de::Error::custom)
287    }
288}
289
290#[cfg(test)]
291mod test {
292    use super::*;
293    use bool_tag_expr::TagValue;
294    use open_timeline_macros::{day, month, year};
295    use std::{
296        collections::BTreeSet,
297        fs::{self, File},
298        io::{self, BufRead},
299        path::PathBuf,
300    };
301
302    const KNOWN_UUIDV4: &str = "6474cd74-244d-449b-a3d1-3a74019ec6f5";
303
304    fn valid_entity() -> Entity {
305        Entity::from(
306            Some(OpenTimelineId::from(KNOWN_UUIDV4).unwrap()),
307            Name::from("Noam").unwrap(),
308            Date::from(None, None, 1111).unwrap(),
309            Some(Date::from(None, None, 2222).unwrap()),
310            Some(Tags::new()),
311        )
312        .unwrap()
313    }
314
315    // TODO (more checks - for tags (if empty set to None instead))
316    #[test]
317    fn from() {
318        let entity = Entity::from(
319            Some(OpenTimelineId::new()),
320            Name::from("Noam").unwrap(),
321            Date::from(None, None, 1111).unwrap(),
322            Some(Date::from(None, None, 2222).unwrap()),
323            Some(Tags::new()),
324        );
325        assert!(entity.is_ok());
326    }
327
328    #[test]
329    fn name_getters_and_setters() {
330        // Get a valid entity
331        let mut entity = valid_entity();
332
333        // Check the name getter
334        assert_eq!(entity.name(), &Name::from("Noam").unwrap());
335
336        // Use the name setter
337        entity.set_name(Name::from("Alan").unwrap());
338
339        // Check the name setter
340        assert_eq!(entity.name(), &Name::from("Alan").unwrap());
341    }
342
343    #[test]
344    fn id_getters_and_setters() {
345        // Get a valid entity
346        let mut entity = valid_entity();
347
348        // Check the ID getter
349        assert_eq!(
350            entity.id(),
351            Some(OpenTimelineId::from(KNOWN_UUIDV4).unwrap())
352        );
353
354        // Get known ID
355        let id = OpenTimelineId::new();
356
357        // Use the ID setter
358        entity.set_id(id);
359
360        // Check the ID setter
361        assert_eq!(entity.id(), Some(id));
362
363        // Use the ID clearer
364        entity.clear_id();
365
366        // Check the ID clearer
367        assert!(entity.id().is_none());
368    }
369
370    #[test]
371    fn date_getters_and_setters() {
372        // Get a valid entity
373        let mut entity = valid_entity();
374
375        let start = entity.start();
376        let end = entity.end().unwrap();
377
378        // Check the start date setter
379
380        // Start after end
381        assert!(
382            entity
383                .set_start(Date::from(Some(1), Some(2), 3333).unwrap())
384                .is_err()
385        );
386        assert_eq!(entity.start(), start);
387
388        // Start before end
389        assert!(
390            entity
391                .set_start(Date::from(Some(1), Some(2), 3).unwrap())
392                .is_ok()
393        );
394        assert_ne!(entity.start(), start);
395
396        // Check the end date setter
397
398        // End before start
399        assert!(
400            entity
401                .set_end(Date::from(Some(4), Some(5), -6543).unwrap())
402                .is_err()
403        );
404        assert_eq!(entity.end().unwrap(), end);
405
406        // End after start
407        assert!(
408            entity
409                .set_end(Date::from(Some(4), Some(5), 6).unwrap())
410                .is_ok()
411        );
412        assert_ne!(entity.end().unwrap(), end);
413
414        // Check the date getters
415        assert_eq!(entity.start_year(), year!(3));
416        assert_eq!(entity.start_month(), Some(month!(2)));
417        assert_eq!(entity.start_day(), Some(day!(1)));
418
419        assert_eq!(entity.end_year(), Some(year!(6)));
420        assert_eq!(entity.end_month(), Some(month!(5)));
421        assert_eq!(entity.end_day(), Some(day!(4)));
422    }
423
424    #[test]
425    fn deserialisation() {
426        let path_to_test_data = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test-data");
427
428        // Check the valid JSON entities can be parsed
429        for entry in fs::read_dir(path_to_test_data.join("entities/valid")).unwrap() {
430            let entry = entry.unwrap();
431            let path = entry.path();
432            if path.is_file() && path.extension().is_some_and(|ext| ext == "jsonc") {
433                let json_content = load_jsonc_strip_leading_comment_lines(&path);
434                println!("Reading file: {:?}", path);
435                println!("{}", json_content);
436                let entities: Result<Vec<Entity>, serde_json::Error> =
437                    serde_json::from_str(&json_content);
438                assert!(entities.is_ok())
439            }
440        }
441
442        // Check the invalid JSON entities cannot be parsed
443        for entry in fs::read_dir(path_to_test_data.join("entities/invalid")).unwrap() {
444            let entry = entry.unwrap();
445            let path = entry.path();
446
447            if path.is_file() && path.extension().is_some_and(|ext| ext == "jsonc") {
448                println!("Reading file: {:?}", path);
449                let json_content = load_jsonc_strip_leading_comment_lines(&path);
450                println!("{}", json_content);
451                let entities: Result<Vec<Entity>, serde_json::Error> =
452                    serde_json::from_str(&json_content);
453                assert!(entities.is_err())
454            }
455        }
456    }
457
458    pub fn load_jsonc_strip_leading_comment_lines(path: &PathBuf) -> String {
459        // Open the file for reading
460        let file = File::open(path).unwrap();
461        let reader = io::BufReader::new(file);
462
463        // Holds the JSON as it's collected
464        let mut json_content = String::new();
465
466        // Collect all lines that don't begin with "//"
467        for line in reader.lines() {
468            let line = line.unwrap();
469            if !line.starts_with("//") {
470                json_content.push_str(&line);
471                json_content.push('\n');
472            }
473        }
474
475        // Return the JSON now that the comment(s) at the top of the file have
476        // been removed
477        json_content
478    }
479
480    #[test]
481    fn matches_bool_tag_expr() -> Result<(), Box<dyn std::error::Error>> {
482        //
483        // 1. expr with 1 tag value
484        //
485
486        // Create bool expr
487        let bool_tag_expr = BoolTagExpr::from("a")?;
488
489        // Add tag to entity
490        let tags = Tags::from([Tag::from(None, TagValue::from("a")?)]);
491        let mut entity_a = valid_entity();
492        entity_a.tags = Some(tags);
493
494        // Should match
495        assert!(entity_a.matches_bool_tag_expr(&bool_tag_expr));
496
497        //
498        // 2. expr with 2 tag values
499        //
500
501        // Create bool expr
502        let bool_tag_expr = BoolTagExpr::from("a & b")?;
503
504        // Add only 1 tag to the entity
505        let tags = Tags::from([Tag::from(None, TagValue::from("a")?)]);
506        let mut entity_a = valid_entity();
507        entity_a.tags = Some(tags);
508
509        // Shouldn't match
510        assert!(!entity_a.matches_bool_tag_expr(&bool_tag_expr));
511
512        // Add 2nd tag to entity
513        entity_a
514            .tags
515            .get_or_insert_with(BTreeSet::new)
516            .insert(Tag::from(None, TagValue::from("b")?));
517
518        // Should match
519        assert!(entity_a.matches_bool_tag_expr(&bool_tag_expr));
520
521        //
522        // 2. expr with 2 tag values, 1 is NOT (reverse expected results of test 2)
523        //
524
525        // Create bool expr (note use of `!`)
526        let bool_tag_expr = BoolTagExpr::from("a & !b")?;
527
528        // Add only 1 tag to the entity
529        let tags = Tags::from([Tag::from(None, TagValue::from("a")?)]);
530        let mut entity_a = valid_entity();
531        entity_a.tags = Some(tags);
532
533        // Should match this time (doesn't have tag `b`)
534        assert!(entity_a.matches_bool_tag_expr(&bool_tag_expr));
535
536        // Add 2nd tag to entity
537        entity_a
538            .tags
539            .get_or_insert_with(BTreeSet::new)
540            .insert(Tag::from(None, TagValue::from("b")?));
541
542        // Shouldn't match
543        assert!(!entity_a.matches_bool_tag_expr(&bool_tag_expr));
544
545        //
546        // 3. Advanced expr
547        //
548
549        // Create advanced bool expr
550        let bool_tag_expr = BoolTagExpr::from("(a | b & c) & !(d & e)")?;
551
552        // Add only tag `a` to the entity (should match)
553        let tags = Tags::from([Tag::from(None, TagValue::from("a")?)]);
554        let mut entity_a = valid_entity();
555        entity_a.tags = Some(tags);
556        assert!(entity_a.matches_bool_tag_expr(&bool_tag_expr));
557
558        // Add only tag `a` to the entity (shouldn't match)
559        let tags = Tags::from([
560            Tag::from(None, TagValue::from("a")?),
561            Tag::from(None, TagValue::from("d")?),
562            Tag::from(None, TagValue::from("e")?),
563        ]);
564        entity_a.tags = Some(tags);
565        assert!(!entity_a.matches_bool_tag_expr(&bool_tag_expr));
566
567        // Add only tag `a` to the entity (shouldn't match)
568        let tags = Tags::from([
569            Tag::from(None, TagValue::from("a")?),
570            Tag::from(None, TagValue::from("b")?),
571            Tag::from(None, TagValue::from("c")?),
572            Tag::from(None, TagValue::from("d")?),
573            Tag::from(None, TagValue::from("superfluous")?),
574        ]);
575        entity_a.tags = Some(tags);
576        assert!(entity_a.matches_bool_tag_expr(&bool_tag_expr));
577
578        Ok(())
579    }
580}