1use 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#[derive(Error, Debug)]
16pub enum EntityError {
17 #[error("The entity dates are invalid")]
18 Dates,
19}
20
21#[derive(Serialize, Clone, Debug, PartialEq, Eq, Hash)]
23pub struct Entity {
24 id: Option<OpenTimelineId>,
26
27 name: Name,
29
30 start: Date,
32
33 end: Option<Date>,
35
36 tags: Option<Tags>,
38}
39
40impl Ord for Entity {
44 fn cmp(&self, other: &Self) -> Ordering {
45 self.id.cmp(&other.id)
46 }
47}
48
49impl PartialOrd for Entity {
51 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
52 Some(self.cmp(other))
53 }
54}
55
56impl Entity {
57 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 pub fn clear_id(&mut self) {
83 self.id = None;
84 }
85
86 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 pub fn tags(&self) -> &Option<Tags> {
98 &self.tags
99 }
100
101 pub fn tags_mut(&mut self) -> &mut Option<Tags> {
103 &mut self.tags
104 }
105
106 pub fn add_tag(&mut self, tag: Tag) {
108 self.tags.get_or_insert_with(Tags::new).insert(tag);
109 }
110
111 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 pub fn set_tags(&mut self, tags: Tags) {
123 self.tags = (!tags.is_empty()).then_some(tags);
124 }
125
126 pub fn clear_tags(&mut self) {
128 self.tags = None;
129 }
130
131 pub fn start(&self) -> Date {
133 self.start
134 }
135
136 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 pub fn end(&self) -> Option<Date> {
149 self.end
150 }
151
152 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 pub fn end_year_is_set(&self) -> bool {
165 self.end_year().is_some()
166 }
167
168 pub fn end_year(&self) -> Option<Year> {
170 self.end.map(|date| date.year())
171 }
172
173 pub fn end_month(&self) -> Option<Month> {
175 self.end.and_then(|date| date.month())
176 }
177
178 pub fn end_day(&self) -> Option<Day> {
180 self.end.and_then(|date| date.day())
181 }
182
183 pub fn start_year(&self) -> Year {
185 self.start.year()
186 }
187
188 pub fn start_month(&self) -> Option<Month> {
190 self.start.month()
191 }
192
193 pub fn start_day(&self) -> Option<Day> {
195 self.start.day()
196 }
197
198 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#[derive(Deserialize, Debug)]
228pub struct RawEndDate {
229 day: Option<i64>,
230 month: Option<i64>,
231 year: Option<i64>,
232}
233
234#[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 let raw_entity = RawEntity::deserialize(deserializer)?;
251
252 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 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 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 #[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 let mut entity = valid_entity();
332
333 assert_eq!(entity.name(), &Name::from("Noam").unwrap());
335
336 entity.set_name(Name::from("Alan").unwrap());
338
339 assert_eq!(entity.name(), &Name::from("Alan").unwrap());
341 }
342
343 #[test]
344 fn id_getters_and_setters() {
345 let mut entity = valid_entity();
347
348 assert_eq!(
350 entity.id(),
351 Some(OpenTimelineId::from(KNOWN_UUIDV4).unwrap())
352 );
353
354 let id = OpenTimelineId::new();
356
357 entity.set_id(id);
359
360 assert_eq!(entity.id(), Some(id));
362
363 entity.clear_id();
365
366 assert!(entity.id().is_none());
368 }
369
370 #[test]
371 fn date_getters_and_setters() {
372 let mut entity = valid_entity();
374
375 let start = entity.start();
376 let end = entity.end().unwrap();
377
378 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 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 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 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 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 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 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 let file = File::open(path).unwrap();
461 let reader = io::BufReader::new(file);
462
463 let mut json_content = String::new();
465
466 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 json_content
478 }
479
480 #[test]
481 fn matches_bool_tag_expr() -> Result<(), Box<dyn std::error::Error>> {
482 let bool_tag_expr = BoolTagExpr::from("a")?;
488
489 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 assert!(entity_a.matches_bool_tag_expr(&bool_tag_expr));
496
497 let bool_tag_expr = BoolTagExpr::from("a & b")?;
503
504 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 assert!(!entity_a.matches_bool_tag_expr(&bool_tag_expr));
511
512 entity_a
514 .tags
515 .get_or_insert_with(BTreeSet::new)
516 .insert(Tag::from(None, TagValue::from("b")?));
517
518 assert!(entity_a.matches_bool_tag_expr(&bool_tag_expr));
520
521 let bool_tag_expr = BoolTagExpr::from("a & !b")?;
527
528 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 assert!(entity_a.matches_bool_tag_expr(&bool_tag_expr));
535
536 entity_a
538 .tags
539 .get_or_insert_with(BTreeSet::new)
540 .insert(Tag::from(None, TagValue::from("b")?));
541
542 assert!(!entity_a.matches_bool_tag_expr(&bool_tag_expr));
544
545 let bool_tag_expr = BoolTagExpr::from("(a | b & c) & !(d & e)")?;
551
552 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 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 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}