diff --git a/Cargo.lock b/Cargo.lock index 97e5942..15e7fb9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -42,16 +42,6 @@ dependencies = [ "serde", ] -[[package]] -name = "chrono-tz" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3" -dependencies = [ - "chrono", - "phf", -] - [[package]] name = "crossbeam-channel" version = "0.5.15" @@ -103,7 +93,6 @@ version = "3.1.4" dependencies = [ "anyhow", "chrono", - "chrono-tz", "notify-debouncer-full", "serde", "serde_json", @@ -270,24 +259,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "phf" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" -dependencies = [ - "phf_shared", -] - -[[package]] -name = "phf_shared" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981" -dependencies = [ - "siphasher", -] - [[package]] name = "proc-macro2" version = "1.0.101" @@ -379,12 +350,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "siphasher" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" - [[package]] name = "smallvec" version = "1.15.1" diff --git a/Cargo.toml b/Cargo.toml index 21e11d7..eb047ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,6 @@ lto = true [dependencies] anyhow = "1" chrono = { version = "0.4", default-features = false, features = ["std", "serde"] } -chrono-tz = "0.10" notify-debouncer-full = "0.3" serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/src/apply_changes.rs b/src/apply_changes.rs index 3c569ec..9b4b183 100644 --- a/src/apply_changes.rs +++ b/src/apply_changes.rs @@ -1,63 +1,44 @@ #![expect(clippy::non_ascii_literal)] +use chrono::NaiveDateTime; + use crate::generate_ics::{EventStatus, SoonToBeIcsEvent}; -use crate::userconfig::{Change, RemovedEvents}; +use crate::userconfig::{Change, RemovedEvents, Userconfig}; -pub fn apply_changes( - events: &mut Vec, - changes: Vec, - removed_events: RemovedEvents, -) -> anyhow::Result<()> { - for change in changes { - apply_change(events, change, removed_events)?; +pub fn apply_changes(events: &mut Vec, userconfig: Userconfig) { + for (name, details) in userconfig.events { + for (date, change) in details.changes { + apply_change(events, &name, date, change, userconfig.removed_events); + } } - Ok(()) } -#[expect(clippy::suspicious_operation_groupings)] fn apply_change( events: &mut Vec, + name: &str, + date: NaiveDateTime, change: Change, removed_events: RemovedEvents, -) -> anyhow::Result<()> { - if change.add { - let end_time = change - .endtime - .ok_or_else(|| anyhow::anyhow!("change add has no end_time specified"))?; - let end_time = change.date.date().and_time(end_time); - - events.push(SoonToBeIcsEvent { - pretty_name: if let Some(namesuffix) = change.namesuffix { - format!("{} {namesuffix}", change.name) - } else { - change.name.clone() - }, - name: change.name, - status: EventStatus::Confirmed, - start_time: change.date, - end_time, - alert_minutes_before: None, - description: "Dies ist eine zusätzliche Veranstaltung welche manuell von dir über den Telegram Bot hinzufügt wurde.".to_owned(), - location: change.room.unwrap_or_default(), - }); - } else if let Some(i) = events +) { + if let Some(i) = events .iter() - .position(|event| event.name == change.name && event.start_time == change.date) + .position(|event| event.name == name && event.start_time == date) { let event = &mut events[i]; if change.remove { match removed_events { RemovedEvents::Cancelled => event.status = EventStatus::Cancelled, - RemovedEvents::Emoji => event.pretty_name = format!("🚫 {}", event.pretty_name), + RemovedEvents::Emoji => event.pretty_name.insert_str(0, "🚫 "), RemovedEvents::Removed => { events.remove(i); - return Ok(()); + return; } } } - if let Some(namesuffix) = change.namesuffix { - event.pretty_name = format!("{} {namesuffix}", event.pretty_name); + if let Some(namesuffix) = &change.namesuffix { + event.pretty_name += " "; + event.pretty_name += namesuffix; } if let Some(room) = change.room { @@ -65,18 +46,16 @@ fn apply_change( } if let Some(time) = change.starttime { - event.start_time = change.date.date().and_time(time); + event.start_time = date.date().and_time(time); } if let Some(time) = change.endtime { - event.end_time = change.date.date().and_time(time); + event.end_time = date.date().and_time(time); } } else { // Event for this change doesnt exist. // This not nice but the TelegramBot has to solve this via user feedback. } - - Ok(()) } #[cfg(test)] @@ -120,20 +99,19 @@ fn generate_events() -> Vec { #[test] fn non_existing_event_of_change_is_skipped() { let mut events = generate_events(); + let name = "BTI5-VS"; + let date = chrono::NaiveDate::from_ymd_opt(2020, 5, 15) + .unwrap() + .and_hms_opt(13, 37, 0) + .unwrap(); let change = Change { - name: "BTI5-VS".to_owned(), - date: chrono::NaiveDate::from_ymd_opt(2020, 5, 15) - .unwrap() - .and_hms_opt(13, 37, 0) - .unwrap(), - add: false, remove: true, starttime: None, endtime: None, namesuffix: None, room: None, }; - apply_change(&mut events, change, RemovedEvents::Cancelled).unwrap(); + apply_change(&mut events, name, date, change, RemovedEvents::Cancelled); assert_eq!(events.len(), 2); let expected = generate_events(); @@ -144,40 +122,38 @@ fn non_existing_event_of_change_is_skipped() { #[test] fn remove_event_is_removed_completly() { let mut events = generate_events(); + let name = "BTI5-VSP/01"; + let date = chrono::NaiveDate::from_ymd_opt(2020, 5, 14) + .unwrap() + .and_hms_opt(8, 15, 0) + .unwrap(); let change = Change { - name: "BTI5-VSP/01".to_owned(), - date: chrono::NaiveDate::from_ymd_opt(2020, 5, 14) - .unwrap() - .and_hms_opt(8, 15, 0) - .unwrap(), - add: false, remove: true, starttime: None, endtime: None, namesuffix: None, room: None, }; - apply_change(&mut events, change, RemovedEvents::Removed).unwrap(); + apply_change(&mut events, name, date, change, RemovedEvents::Removed); assert_eq!(events.len(), 1); } #[test] fn remove_event_gets_marked_as_cancelled() { let mut events = generate_events(); + let name = "BTI5-VSP/01"; + let date = chrono::NaiveDate::from_ymd_opt(2020, 5, 14) + .unwrap() + .and_hms_opt(8, 15, 0) + .unwrap(); let change = Change { - name: "BTI5-VSP/01".to_owned(), - date: chrono::NaiveDate::from_ymd_opt(2020, 5, 14) - .unwrap() - .and_hms_opt(8, 15, 0) - .unwrap(), - add: false, remove: true, starttime: None, endtime: None, namesuffix: None, room: None, }; - apply_change(&mut events, change, RemovedEvents::Cancelled).unwrap(); + apply_change(&mut events, name, date, change, RemovedEvents::Cancelled); assert_eq!(events.len(), 2); assert_eq!(events[1].status, EventStatus::Cancelled); } @@ -185,20 +161,19 @@ fn remove_event_gets_marked_as_cancelled() { #[test] fn remove_event_gets_emoji_prefix() { let mut events = generate_events(); + let name = "BTI5-VSP/01"; + let date = chrono::NaiveDate::from_ymd_opt(2020, 5, 14) + .unwrap() + .and_hms_opt(8, 15, 0) + .unwrap(); let change = Change { - name: "BTI5-VSP/01".to_owned(), - date: chrono::NaiveDate::from_ymd_opt(2020, 5, 14) - .unwrap() - .and_hms_opt(8, 15, 0) - .unwrap(), - add: false, remove: true, starttime: None, endtime: None, namesuffix: None, room: None, }; - apply_change(&mut events, change, RemovedEvents::Emoji).unwrap(); + apply_change(&mut events, name, date, change, RemovedEvents::Emoji); assert_eq!(events.len(), 2); assert_eq!(events[1].pretty_name, "🚫 BTI5-VSP/01"); } @@ -206,60 +181,57 @@ fn remove_event_gets_emoji_prefix() { #[test] fn namesuffix_is_added() { let mut events = generate_events(); + let name = "BTI5-VSP/01"; + let date = chrono::NaiveDate::from_ymd_opt(2020, 5, 14) + .unwrap() + .and_hms_opt(8, 15, 0) + .unwrap(); let change = Change { - name: "BTI5-VSP/01".to_owned(), - date: chrono::NaiveDate::from_ymd_opt(2020, 5, 14) - .unwrap() - .and_hms_opt(8, 15, 0) - .unwrap(), - add: false, remove: false, starttime: None, endtime: None, namesuffix: Some("whatever".to_owned()), room: None, }; - apply_change(&mut events, change, RemovedEvents::Cancelled).unwrap(); + apply_change(&mut events, name, date, change, RemovedEvents::Cancelled); assert_eq!(events[1].pretty_name, "BTI5-VSP/01 whatever"); } #[test] fn room_is_overwritten() { let mut events = generate_events(); + let name = "BTI5-VSP/01"; + let date = chrono::NaiveDate::from_ymd_opt(2020, 5, 14) + .unwrap() + .and_hms_opt(8, 15, 0) + .unwrap(); let change = Change { - name: "BTI5-VSP/01".to_owned(), - date: chrono::NaiveDate::from_ymd_opt(2020, 5, 14) - .unwrap() - .and_hms_opt(8, 15, 0) - .unwrap(), - add: false, remove: false, starttime: None, endtime: None, namesuffix: None, room: Some("whereever".to_owned()), }; - apply_change(&mut events, change, RemovedEvents::Cancelled).unwrap(); + apply_change(&mut events, name, date, change, RemovedEvents::Cancelled); assert_eq!(events[1].location, "whereever"); } #[test] fn starttime_changed() { let mut events = generate_events(); + let name = "BTI5-VSP/01"; + let date = chrono::NaiveDate::from_ymd_opt(2020, 5, 14) + .unwrap() + .and_hms_opt(8, 15, 0) + .unwrap(); let change = Change { - name: "BTI5-VSP/01".to_owned(), - date: chrono::NaiveDate::from_ymd_opt(2020, 5, 14) - .unwrap() - .and_hms_opt(8, 15, 0) - .unwrap(), - add: false, remove: false, starttime: Some(chrono::NaiveTime::from_hms_opt(8, 30, 0).unwrap()), endtime: None, namesuffix: None, room: None, }; - apply_change(&mut events, change, RemovedEvents::Cancelled).unwrap(); + apply_change(&mut events, name, date, change, RemovedEvents::Cancelled); assert_eq!( events[1].start_time, chrono::NaiveDate::from_ymd_opt(2020, 5, 14) @@ -272,20 +244,19 @@ fn starttime_changed() { #[test] fn endtime_changed() { let mut events = generate_events(); + let name = "BTI5-VSP/01"; + let date = chrono::NaiveDate::from_ymd_opt(2020, 5, 14) + .unwrap() + .and_hms_opt(8, 15, 0) + .unwrap(); let change = Change { - name: "BTI5-VSP/01".to_owned(), - date: chrono::NaiveDate::from_ymd_opt(2020, 5, 14) - .unwrap() - .and_hms_opt(8, 15, 0) - .unwrap(), - add: false, remove: false, starttime: None, endtime: Some(chrono::NaiveTime::from_hms_opt(8, 30, 0).unwrap()), namesuffix: None, room: None, }; - apply_change(&mut events, change, RemovedEvents::Cancelled).unwrap(); + apply_change(&mut events, name, date, change, RemovedEvents::Cancelled); assert_eq!( events[1].end_time, chrono::NaiveDate::from_ymd_opt(2020, 5, 14) @@ -294,39 +265,3 @@ fn endtime_changed() { .unwrap() ); } - -#[test] -fn event_added() { - let mut events = generate_events(); - let change = Change { - name: "BTI5-VSP/01".to_owned(), - date: chrono::NaiveDate::from_ymd_opt(2020, 5, 30) - .unwrap() - .and_hms_opt(10, 0, 0) - .unwrap(), - add: true, - remove: false, - starttime: None, - endtime: Some(chrono::NaiveTime::from_hms_opt(10, 30, 0).unwrap()), - namesuffix: None, - room: None, - }; - apply_change(&mut events, change, RemovedEvents::Cancelled).unwrap(); - assert_eq!(events.len(), 3); - assert_eq!(events[2].name, "BTI5-VSP/01"); - assert_eq!( - events[2].start_time, - chrono::NaiveDate::from_ymd_opt(2020, 5, 30) - .unwrap() - .and_hms_opt(10, 0, 0) - .unwrap() - ); - assert_eq!( - events[2].end_time, - chrono::NaiveDate::from_ymd_opt(2020, 5, 30) - .unwrap() - .and_hms_opt(10, 30, 0) - .unwrap() - ); - assert_eq!(events[2].location, ""); -} diff --git a/src/apply_details.rs b/src/apply_details.rs index 9a9caf0..6d05985 100644 --- a/src/apply_details.rs +++ b/src/apply_details.rs @@ -39,7 +39,7 @@ fn create_event(description: &str) -> SoonToBeIcsEvent { fn check_alert(alert_minutes_before: Option) { let details = EventDetails { alert_minutes_before, - notes: None, + ..EventDetails::default() }; let mut event = create_event(""); apply_details(&mut event, &details); @@ -57,8 +57,8 @@ fn alert_examples() { #[cfg(test)] fn check_description(notes: Option<&str>, event_description: &str, expected: &str) { let details = EventDetails { - alert_minutes_before: None, notes: notes.map(ToOwned::to_owned), + ..EventDetails::default() }; let mut event = create_event(event_description); apply_details(&mut event, &details); diff --git a/src/output_files.rs b/src/output_files.rs index bc7508a..a90effc 100644 --- a/src/output_files.rs +++ b/src/output_files.rs @@ -85,13 +85,6 @@ fn one_internal(content: UserconfigFile) -> anyhow::Result { }); } - apply_changes( - &mut user_events, - content.config.changes, - content.config.removed_events, - ) - .context("failed to apply changes")?; - for event in &mut user_events { if let Some(details) = content.config.events.get(&event.name) { apply_details(event, details); @@ -102,6 +95,8 @@ fn one_internal(content: UserconfigFile) -> anyhow::Result { } } + apply_changes(&mut user_events, content.config); + user_events.sort_by_cached_key(|event| event.start_time); let ics_content = generate_ics(&first_name, &user_events); diff --git a/src/userconfig.rs b/src/userconfig.rs index 88127ef..a97117e 100644 --- a/src/userconfig.rs +++ b/src/userconfig.rs @@ -1,7 +1,6 @@ use std::collections::HashMap; -use chrono::{NaiveDateTime, NaiveTime, TimeZone as _}; -use chrono_tz::Europe::Berlin; +use chrono::{NaiveDateTime, NaiveTime}; use serde::Deserialize; #[derive(Deserialize, Debug)] @@ -25,12 +24,14 @@ pub enum RemovedEvents { Emoji, } -#[derive(Deserialize, Debug)] +#[derive(Debug, Default, Deserialize)] #[serde(rename_all = "camelCase")] pub struct EventDetails { #[serde(default)] pub alert_minutes_before: Option, #[serde(default)] + pub changes: HashMap, + #[serde(default)] pub notes: Option, } @@ -39,70 +40,27 @@ pub struct EventDetails { pub struct Userconfig { pub calendarfile_suffix: String, - #[serde(default)] - pub changes: Vec, - pub events: HashMap, #[serde(default)] pub removed_events: RemovedEvents, } -#[derive(Deserialize, Debug)] +#[derive(Debug, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct Change { - pub name: String, - - #[serde(deserialize_with = "deserialize_change_date")] - pub date: NaiveDateTime, - - #[serde(default)] - pub add: bool, #[serde(default)] pub remove: bool, - #[serde(default, deserialize_with = "deserialize_change_time")] - /// Used when adapting events + #[serde(default)] pub starttime: Option, - #[serde(default, deserialize_with = "deserialize_change_time")] - /// Used for adapting and creating new events + #[serde(default)] pub endtime: Option, pub namesuffix: Option, pub room: Option, } -fn deserialize_change_time<'de, D>(deserializer: D) -> Result, D::Error> -where - D: serde::Deserializer<'de>, -{ - let str = String::deserialize(deserializer)?; - let time = NaiveTime::parse_from_str(&str, "%H:%M").map_err(serde::de::Error::custom)?; - Ok(Some(time)) -} - -fn deserialize_change_date<'de, D>(deserializer: D) -> Result -where - D: serde::Deserializer<'de>, -{ - let raw = String::deserialize(deserializer)?; - parse_change_date(&raw).map_err(serde::de::Error::custom) -} - -fn parse_change_date(raw: &str) -> Result { - let tless = raw.replace('T', " "); - let utc = NaiveDateTime::parse_from_str(&tless, "%Y-%m-%d %H:%M")?; - let date_time = Berlin.from_utc_datetime(&utc); - let naive = date_time.naive_local(); - Ok(naive) -} - -#[test] -fn can_parse_change_date_from_utc_to_local() { - let actual = parse_change_date("2020-07-01T06:30").unwrap(); - let string = actual.and_local_timezone(Berlin).unwrap().to_rfc3339(); - assert_eq!(string, "2020-07-01T08:30:00+02:00"); -} - #[test] fn can_deserialize_chat() -> Result<(), serde_json::Error> { let test: Chat = serde_json::from_str( @@ -117,8 +75,7 @@ fn can_deserialize_chat() -> Result<(), serde_json::Error> { #[test] fn error_on_userconfig_without_calendarfile_suffix() { - let test: Result = - serde_json::from_str(r#"{"changes": [], "events": []}"#); + let test: Result = serde_json::from_str(r#"{"events": []}"#); let error = test.expect_err("parsing should fail"); assert!(error.is_data()); @@ -127,10 +84,9 @@ fn error_on_userconfig_without_calendarfile_suffix() { #[test] fn can_deserialize_minimal_userconfig() -> Result<(), serde_json::Error> { let test: Userconfig = - serde_json::from_str(r#"{"calendarfileSuffix": "123qwe", "changes": [], "events": {}}"#)?; + serde_json::from_str(r#"{"calendarfileSuffix": "123qwe", "events": {}}"#)?; assert_eq!(test.calendarfile_suffix, "123qwe"); - assert_eq!(test.changes.len(), 0); assert_eq!(test.events.len(), 0); assert_eq!(test.removed_events, RemovedEvents::Cancelled); @@ -140,11 +96,10 @@ fn can_deserialize_minimal_userconfig() -> Result<(), serde_json::Error> { #[test] fn can_deserialize_userconfig_with_event_map() -> Result<(), serde_json::Error> { let test: Userconfig = serde_json::from_str( - r#"{"calendarfileSuffix": "123qwe", "changes": [], "events": {"BTI1-TI": {}, "BTI5-VS": {}}, "removedEvents": "removed"}"#, + r#"{"calendarfileSuffix": "123qwe", "events": {"BTI1-TI": {}, "BTI5-VS": {}}, "removedEvents": "removed"}"#, )?; assert_eq!(test.calendarfile_suffix, "123qwe"); - assert_eq!(test.changes.len(), 0); assert_eq!(test.removed_events, RemovedEvents::Removed); let mut events = test.events.keys().collect::>(); @@ -154,68 +109,65 @@ fn can_deserialize_userconfig_with_event_map() -> Result<(), serde_json::Error> Ok(()) } +#[cfg(test)] +#[track_caller] +fn case_change(json: &str, expected: &Change) { + let expected_date = chrono::NaiveDate::from_ymd_opt(2020, 12, 20) + .unwrap() + .and_hms_opt(22, 4, 0) + .unwrap(); + let config = serde_json::from_str::( + & r#"{"calendarfileSuffix": "123qwe", "events": {"Fancy Event Name": {"changes": {"2020-12-20T22:04:00": {}}}}}"#.replace("{}", json), + ).expect("should be able to parse json to userconfig"); + dbg!(&config); + let actual = config + .events + .get("Fancy Event Name") + .expect("event should exist") + .changes + .get(&expected_date) + .expect("date should exist"); + assert_eq!(actual, expected); +} + #[test] -fn can_deserialize_minimal_change() -> Result<(), serde_json::Error> { - let test: Change = serde_json::from_str(r#"{"name": "Tree", "date": "2020-12-20T22:04"}"#)?; - assert_eq!(test.name, "Tree"); - assert_eq!( - test.date, - chrono::NaiveDate::from_ymd_opt(2020, 12, 20) - .unwrap() - .and_hms_opt(23, 4, 0) - .unwrap() +fn can_deserialize_minimal_change() { + case_change( + "{}", + &Change { + remove: false, + starttime: None, + endtime: None, + namesuffix: None, + room: None, + }, ); - assert!(!test.add); - assert!(!test.remove); - assert_eq!(test.starttime, None); - assert_eq!(test.endtime, None); - assert_eq!(test.namesuffix, None); - assert_eq!(test.room, None); - Ok(()) } #[test] -fn can_deserialize_change_remove() -> Result<(), serde_json::Error> { - let test: Change = - serde_json::from_str(r#"{"name": "Tree", "date": "2020-12-20T22:04", "remove": true}"#)?; - assert_eq!(test.name, "Tree"); - assert_eq!( - test.date, - chrono::NaiveDate::from_ymd_opt(2020, 12, 20) - .unwrap() - .and_hms_opt(23, 4, 0) - .unwrap() +fn can_deserialize_change_remove() { + case_change( + r#"{"remove": true}"#, + &Change { + remove: true, + starttime: None, + endtime: None, + namesuffix: None, + room: None, + }, ); - assert!(!test.add); - assert!(test.remove); - assert_eq!(test.starttime, None); - assert_eq!(test.endtime, None); - assert_eq!(test.namesuffix, None); - assert_eq!(test.room, None); - Ok(()) } #[test] -fn can_deserialize_change_add() -> Result<(), serde_json::Error> { - let test: Change = serde_json::from_str( - r#"{"name": "Tree", "date": "2020-12-20T22:04", "add": true, "endtime": "23:42"}"#, - )?; - assert_eq!(test.name, "Tree"); - assert_eq!( - test.date, - chrono::NaiveDate::from_ymd_opt(2020, 12, 20) - .unwrap() - .and_hms_opt(23, 4, 0) - .unwrap() - ); - assert!(test.add); - assert!(!test.remove); - assert_eq!(test.starttime, None); - assert_eq!( - test.endtime, - Some(NaiveTime::from_hms_opt(23, 42, 0).unwrap()) +fn can_deserialize_change_endtime() { + case_change( + r#"{"endtime": "23:42:00"}"#, + &Change { + remove: false, + starttime: None, + endtime: Some(NaiveTime::from_hms_opt(23, 42, 0).unwrap()), + namesuffix: None, + room: None, + }, ); - assert_eq!(test.namesuffix, None); - assert_eq!(test.room, None); - Ok(()) }