use crate::{Date, Day, HasIdAndName, Month, Name, OpenTimelineId, Year};
use bool_tag_expr::{BoolTagExpr, Tag, Tags};
use serde::{Deserialize, Deserializer, Serialize};
use std::cmp::Ordering;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum EntityError {
#[error("The entity dates are invalid")]
Dates,
}
#[derive(Serialize, Clone, Debug, PartialEq, Eq, Hash)]
pub struct Entity {
id: Option<OpenTimelineId>,
name: Name,
start: Date,
end: Option<Date>,
tags: Option<Tags>,
}
impl Ord for Entity {
fn cmp(&self, other: &Self) -> Ordering {
self.id.cmp(&other.id)
}
}
impl PartialOrd for Entity {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Entity {
pub fn from(
id: Option<OpenTimelineId>,
name: Name,
start: Date,
end: Option<Date>,
tags: Option<Tags>,
) -> Result<Entity, EntityError> {
let entity = Entity {
id,
name,
start,
end,
tags,
};
if entity.has_valid_dates() {
Ok(entity)
} else {
Err(EntityError::Dates)
}
}
pub fn clear_id(&mut self) {
self.id = None;
}
fn has_valid_dates(&self) -> bool {
if let Some(end) = &self.end {
if end < &self.start {
return false;
}
}
true
}
pub fn tags(&self) -> &Option<Tags> {
&self.tags
}
pub fn tags_mut(&mut self) -> &mut Option<Tags> {
&mut self.tags
}
pub fn add_tag(&mut self, tag: Tag) {
self.tags.get_or_insert_with(Tags::new).insert(tag);
}
pub fn remove_tag(&mut self, tag: &Tag) {
if let Some(tags) = self.tags.as_mut() {
tags.remove(tag);
if tags.is_empty() {
self.tags = None
}
}
}
pub fn set_tags(&mut self, tags: Tags) {
self.tags = (!tags.is_empty()).then_some(tags);
}
pub fn clear_tags(&mut self) {
self.tags = None;
}
pub fn start(&self) -> Date {
self.start
}
pub fn set_start(&mut self, start: Date) -> Result<(), EntityError> {
let mut tmp_entity = self.clone();
tmp_entity.start = start;
if !tmp_entity.has_valid_dates() {
return Err(EntityError::Dates);
}
self.start = start;
Ok(())
}
pub fn end(&self) -> Option<Date> {
self.end
}
pub fn set_end(&mut self, end: Date) -> Result<(), EntityError> {
let mut tmp_entity = self.clone();
tmp_entity.end = Some(end);
if !tmp_entity.has_valid_dates() {
return Err(EntityError::Dates);
}
self.end = Some(end);
Ok(())
}
pub fn end_year_is_set(&self) -> bool {
self.end_year().is_some()
}
pub fn end_year(&self) -> Option<Year> {
self.end.map(|date| date.year())
}
pub fn end_month(&self) -> Option<Month> {
self.end.and_then(|date| date.month())
}
pub fn end_day(&self) -> Option<Day> {
self.end.and_then(|date| date.day())
}
pub fn start_year(&self) -> Year {
self.start.year()
}
pub fn start_month(&self) -> Option<Month> {
self.start.month()
}
pub fn start_day(&self) -> Option<Day> {
self.start.day()
}
pub fn matches_bool_tag_expr(&self, bool_tag_expr: &BoolTagExpr) -> bool {
let Some(tags) = self.tags() else {
return false;
};
bool_tag_expr.matches(tags)
}
}
impl HasIdAndName for Entity {
fn id(&self) -> Option<OpenTimelineId> {
self.id
}
fn set_id(&mut self, id: OpenTimelineId) {
self.id = Some(id)
}
fn name(&self) -> &Name {
&self.name
}
fn set_name(&mut self, name: Name) {
self.name = name
}
}
#[derive(Deserialize, Debug)]
pub struct RawEndDate {
day: Option<i64>,
month: Option<i64>,
year: Option<i64>,
}
#[derive(Deserialize, Debug)]
struct RawEntity {
id: Option<OpenTimelineId>,
name: Name,
start: Date,
end: Option<RawEndDate>,
tags: Option<Tags>,
}
impl<'de> Deserialize<'de> for Entity {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let raw_entity = RawEntity::deserialize(deserializer)?;
let end = match raw_entity.end {
None => None,
Some(end) => {
if end.day.is_none() && end.month.is_none() && end.year.is_none() {
None
} else if end.year.is_none() {
let err_msg = String::from(
"End year is invalid (day and/or month is set, but year isn't",
);
return Err(serde::de::Error::custom(err_msg));
} else {
match Date::from(end.day, end.month, end.year.unwrap()) {
Ok(end) => Some(end),
Err(_) => {
let err_msg = String::from("End year is invalid");
return Err(serde::de::Error::custom(err_msg));
}
}
}
}
};
Entity::from(
raw_entity.id,
raw_entity.name,
raw_entity.start,
end,
raw_entity.tags,
)
.map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod test {
use super::*;
use bool_tag_expr::TagValue;
use open_timeline_macros::{day, month, year};
use std::{
collections::BTreeSet,
fs::{self, File},
io::{self, BufRead},
path::PathBuf,
};
const KNOWN_UUIDV4: &str = "6474cd74-244d-449b-a3d1-3a74019ec6f5";
fn valid_entity() -> Entity {
Entity::from(
Some(OpenTimelineId::from(KNOWN_UUIDV4).unwrap()),
Name::from("Noam").unwrap(),
Date::from(None, None, 1111).unwrap(),
Some(Date::from(None, None, 2222).unwrap()),
Some(Tags::new()),
)
.unwrap()
}
#[test]
fn from() {
let entity = Entity::from(
Some(OpenTimelineId::new()),
Name::from("Noam").unwrap(),
Date::from(None, None, 1111).unwrap(),
Some(Date::from(None, None, 2222).unwrap()),
Some(Tags::new()),
);
assert!(entity.is_ok());
}
#[test]
fn name_getters_and_setters() {
let mut entity = valid_entity();
assert_eq!(entity.name(), &Name::from("Noam").unwrap());
entity.set_name(Name::from("Alan").unwrap());
assert_eq!(entity.name(), &Name::from("Alan").unwrap());
}
#[test]
fn id_getters_and_setters() {
let mut entity = valid_entity();
assert_eq!(
entity.id(),
Some(OpenTimelineId::from(KNOWN_UUIDV4).unwrap())
);
let id = OpenTimelineId::new();
entity.set_id(id);
assert_eq!(entity.id(), Some(id));
entity.clear_id();
assert!(entity.id().is_none());
}
#[test]
fn date_getters_and_setters() {
let mut entity = valid_entity();
let start = entity.start();
let end = entity.end().unwrap();
assert!(
entity
.set_start(Date::from(Some(1), Some(2), 3333).unwrap())
.is_err()
);
assert_eq!(entity.start(), start);
assert!(
entity
.set_start(Date::from(Some(1), Some(2), 3).unwrap())
.is_ok()
);
assert_ne!(entity.start(), start);
assert!(
entity
.set_end(Date::from(Some(4), Some(5), -6543).unwrap())
.is_err()
);
assert_eq!(entity.end().unwrap(), end);
assert!(
entity
.set_end(Date::from(Some(4), Some(5), 6).unwrap())
.is_ok()
);
assert_ne!(entity.end().unwrap(), end);
assert_eq!(entity.start_year(), year!(3));
assert_eq!(entity.start_month(), Some(month!(2)));
assert_eq!(entity.start_day(), Some(day!(1)));
assert_eq!(entity.end_year(), Some(year!(6)));
assert_eq!(entity.end_month(), Some(month!(5)));
assert_eq!(entity.end_day(), Some(day!(4)));
}
#[test]
fn deserialisation() {
let path_to_test_data = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test-data");
for entry in fs::read_dir(path_to_test_data.join("entities/valid")).unwrap() {
let entry = entry.unwrap();
let path = entry.path();
if path.is_file() && path.extension().is_some_and(|ext| ext == "jsonc") {
let json_content = load_jsonc_strip_leading_comment_lines(&path);
println!("Reading file: {:?}", path);
println!("{}", json_content);
let entities: Result<Vec<Entity>, serde_json::Error> =
serde_json::from_str(&json_content);
assert!(entities.is_ok())
}
}
for entry in fs::read_dir(path_to_test_data.join("entities/invalid")).unwrap() {
let entry = entry.unwrap();
let path = entry.path();
if path.is_file() && path.extension().is_some_and(|ext| ext == "jsonc") {
println!("Reading file: {:?}", path);
let json_content = load_jsonc_strip_leading_comment_lines(&path);
println!("{}", json_content);
let entities: Result<Vec<Entity>, serde_json::Error> =
serde_json::from_str(&json_content);
assert!(entities.is_err())
}
}
}
pub fn load_jsonc_strip_leading_comment_lines(path: &PathBuf) -> String {
let file = File::open(path).unwrap();
let reader = io::BufReader::new(file);
let mut json_content = String::new();
for line in reader.lines() {
let line = line.unwrap();
if !line.starts_with("//") {
json_content.push_str(&line);
json_content.push('\n');
}
}
json_content
}
#[test]
fn matches_bool_tag_expr() -> Result<(), Box<dyn std::error::Error>> {
let bool_tag_expr = BoolTagExpr::from("a")?;
let tags = Tags::from([Tag::from(None, TagValue::from("a")?)]);
let mut entity_a = valid_entity();
entity_a.tags = Some(tags);
assert!(entity_a.matches_bool_tag_expr(&bool_tag_expr));
let bool_tag_expr = BoolTagExpr::from("a & b")?;
let tags = Tags::from([Tag::from(None, TagValue::from("a")?)]);
let mut entity_a = valid_entity();
entity_a.tags = Some(tags);
assert!(!entity_a.matches_bool_tag_expr(&bool_tag_expr));
entity_a
.tags
.get_or_insert_with(BTreeSet::new)
.insert(Tag::from(None, TagValue::from("b")?));
assert!(entity_a.matches_bool_tag_expr(&bool_tag_expr));
let bool_tag_expr = BoolTagExpr::from("a & !b")?;
let tags = Tags::from([Tag::from(None, TagValue::from("a")?)]);
let mut entity_a = valid_entity();
entity_a.tags = Some(tags);
assert!(entity_a.matches_bool_tag_expr(&bool_tag_expr));
entity_a
.tags
.get_or_insert_with(BTreeSet::new)
.insert(Tag::from(None, TagValue::from("b")?));
assert!(!entity_a.matches_bool_tag_expr(&bool_tag_expr));
let bool_tag_expr = BoolTagExpr::from("(a | b & c) & !(d & e)")?;
let tags = Tags::from([Tag::from(None, TagValue::from("a")?)]);
let mut entity_a = valid_entity();
entity_a.tags = Some(tags);
assert!(entity_a.matches_bool_tag_expr(&bool_tag_expr));
let tags = Tags::from([
Tag::from(None, TagValue::from("a")?),
Tag::from(None, TagValue::from("d")?),
Tag::from(None, TagValue::from("e")?),
]);
entity_a.tags = Some(tags);
assert!(!entity_a.matches_bool_tag_expr(&bool_tag_expr));
let tags = Tags::from([
Tag::from(None, TagValue::from("a")?),
Tag::from(None, TagValue::from("b")?),
Tag::from(None, TagValue::from("c")?),
Tag::from(None, TagValue::from("d")?),
Tag::from(None, TagValue::from("superfluous")?),
]);
entity_a.tags = Some(tags);
assert!(entity_a.matches_bool_tag_expr(&bool_tag_expr));
Ok(())
}
}