commit e79ffd7b92c4807a481f6ad3d27605e484f4f58d from: witcher date: Fri Jan 6 19:19:29 2023 UTC Restrict field access on `Task` The `Task` struct has 2 fields that rely on each other: `description` and `tags`. Changing one will most likely mean a change in the other, and changing `tags` by itself probably makes no sense. Both fields have been made private, forcing instantiation with `Task::new` and forcing changes to go through the `set_description` method, both computing the `tags` from the description. commit - a6078516315466372617a4c3b72a6b88d04ad76b commit + e79ffd7b92c4807a481f6ad3d27605e484f4f58d blob - 93be29963141c72b6fc164cf97828d4c2448738c blob + 38e4d1b8f5e505c581996508721002c641872c81 --- src/lib.rs +++ src/lib.rs @@ -33,17 +33,12 @@ use nom::Finish; /// This is the main entry point of the library. /// /// ```rust -/// # use todotxt_parser::{Completion, Task, TodoError, parse_from_str}; +/// # use todotxt_parser::{Completion, Tag, Task, TodoError, parse_from_str}; /// # use chrono::NaiveDate; /// let input = "2023-01-05 sample task"; /// let res = parse_from_str(input)?; -/// assert_eq!(res, vec![Task { -/// completion: Completion::Incomplete, -/// priority: 0, -/// creation_date: Some(NaiveDate::from_ymd_opt(2023, 1, 5).unwrap()), -/// description: "sample task", -/// tags: vec![] -/// }]); +/// assert_eq!(res, vec![Task::new(Completion::Incomplete, 0, Some(NaiveDate::from_ymd_opt(2023, 1, 5).unwrap()), "sample task")]); +/// assert_eq!(res.iter().map(Task::tags).collect::>>(), vec![&vec![]]); /// # Ok::<(), TodoError>(()) /// ``` /// blob - ff10e0718af6f6f74f07409dfa6045cc7afcbb4d blob + f7807f7a2f69ab415d0e9463f3b32ef1bafe4f90 --- src/parser.rs +++ src/parser.rs @@ -517,86 +517,73 @@ mod tests { #[test] fn test_task() { + let (_, t1) = + task("(A) 2023-01-01 a perfectly normal task +testing @home due:2023-01-02").unwrap(); + let tags1 = vec![ + Tag::Project("testing"), + Tag::Context("home"), + Tag::Due(NaiveDate::from_ymd_opt(2023, 01, 02).unwrap()), + ]; assert_eq!( - task("(A) 2023-01-01 a perfectly normal task +testing @home due:2023-01-02"), - Ok(( - "", - Task { - completion: Completion::Incomplete, - priority: 1, - creation_date: Some(NaiveDate::from_ymd_opt(2023, 01, 01).unwrap()), - description: "a perfectly normal task +testing @home due:2023-01-02", - tags: vec![ - Tag::Project("testing"), - Tag::Context("home"), - Tag::Due(NaiveDate::from_ymd_opt(2023, 01, 02).unwrap()) - ], - } - )) + t1, + Task::new( + Completion::Incomplete, + 1, + Some(NaiveDate::from_ymd_opt(2023, 01, 01).unwrap()), + "a perfectly normal task +testing @home due:2023-01-02" + ) ); + assert_eq!(t1.tags(), &tags1); + let (_ ,t2) = task("x 2023-01-02 2023-01-01 a completed task with completion date +testing @home due:2023-01-02 @place +todotxt-parser").unwrap(); + let tags2 = vec![ + Tag::Project("testing"), + Tag::Context("home"), + Tag::Due(NaiveDate::from_ymd_opt(2023, 01, 02).unwrap()), + Tag::Context("place"), + Tag::Project("todotxt-parser"), + ]; assert_eq!( - task("x 2023-01-02 2023-01-01 a completed task with completion date +testing @home due:2023-01-02 @place +todotxt-parser"), - Ok(( - "", - Task { - completion: Completion::Complete(Some(NaiveDate::from_ymd_opt(2023, 01, 02).unwrap())), - priority: 0, - creation_date: Some(NaiveDate::from_ymd_opt(2023, 01, 01).unwrap()), - description: "a completed task with completion date +testing @home due:2023-01-02 @place +todotxt-parser", - tags: vec![ - Tag::Project("testing"), - Tag::Context("home"), - Tag::Due(NaiveDate::from_ymd_opt(2023,01,02).unwrap()), - Tag::Context("place"), - Tag::Project("todotxt-parser"), - ], - }, - )) + t2, + Task::new(Completion::Complete(Some(NaiveDate::from_ymd_opt(2023, 01, 02).unwrap())), 0, Some(NaiveDate::from_ymd_opt(2023, 01, 01).unwrap()), "a completed task with completion date +testing @home due:2023-01-02 @place +todotxt-parser") ); + assert_eq!(t2.tags(), &tags2); } #[test] fn test_tasks() { - assert_eq!( - tasks( + let (_, ts) = tasks( "(A) 2023-01-01 a perfectly normal task +testing @home due:2023-01-02\n\ x 2023-01-02 2023-01-01 a completed task with completion date +testing @home due:2023-01-02 @place +todotxt-parser and recurring every rec:+4m 4 months strictly" - ), - Ok(( - "", - vec![ - Task { - completion: Completion::Incomplete, - priority: 1, - creation_date: Some(NaiveDate::from_ymd_opt(2023, 01, 01).unwrap()), - description: "a perfectly normal task +testing @home due:2023-01-02", - tags: vec![ - Tag::Project("testing"), - Tag::Context("home"), - Tag::Due(NaiveDate::from_ymd_opt(2023,01,02).unwrap()), - ], - }, - Task { - completion: Completion::Complete(Some(NaiveDate::from_ymd_opt(2023, 01, 02).unwrap())), - priority: 0, - creation_date: Some(NaiveDate::from_ymd_opt(2023, 01, 01).unwrap()), - description: "a completed task with completion date +testing @home due:2023-01-02 @place +todotxt-parser and recurring every rec:+4m 4 months strictly", - tags: vec![ - Tag::Project("testing"), - Tag::Context("home"), - Tag::Due(NaiveDate::from_ymd_opt(2023,01,02).unwrap()), - Tag::Context("place"), - Tag::Project("todotxt-parser"), - Tag::Rec(Recurring { - strict: true, - amount: TimeAmount { - amount:4, unit :TimeUnit::Month}}) - ], - }, - ], - )) + ).unwrap(); + let tags1 = vec![ + Tag::Project("testing"), + Tag::Context("home"), + Tag::Due(NaiveDate::from_ymd_opt(2023, 01, 02).unwrap()), + ]; + let tags2 = vec![ + Tag::Project("testing"), + Tag::Context("home"), + Tag::Due(NaiveDate::from_ymd_opt(2023, 01, 02).unwrap()), + Tag::Context("place"), + Tag::Project("todotxt-parser"), + Tag::Rec(Recurring { + strict: true, + amount: TimeAmount { + amount: 4, + unit: TimeUnit::Month, + }, + }), + ]; + assert_eq!( + ts, + vec![ + Task::new(Completion::Incomplete, 1, Some(NaiveDate::from_ymd_opt(2023, 01, 01).unwrap()), "a perfectly normal task +testing @home due:2023-01-02"), + Task::new(Completion::Complete(Some(NaiveDate::from_ymd_opt(2023, 01, 02).unwrap())), 0, Some(NaiveDate::from_ymd_opt(2023, 01, 01).unwrap()), "a completed task with completion date +testing @home due:2023-01-02 @place +todotxt-parser and recurring every rec:+4m 4 months strictly") + ] ); + assert_eq!(ts[0].tags(), &tags1); + assert_eq!(ts[1].tags(), &tags2); } #[test] blob - 3ad4f7ef9998a63833bd53630e40304a3edcd283 blob + d0dd3c3b174c908a313e24cb9f73e95a9a0614d4 --- src/types.rs +++ src/types.rs @@ -76,7 +76,7 @@ impl<'a> Tag<'a> { /// /// This struct describes how recurring should be handled, and can be used to create another task /// when it gets completed, according to its rules. -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Recurring { /// Whether recurring should be strict or not. /// @@ -91,7 +91,7 @@ pub struct Recurring { } /// A unit of time applicable for the [`Recurring`] tag. -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum TimeUnit { /// A day. Day, @@ -118,7 +118,7 @@ impl TryFrom for TimeUnit { } /// An amount of time, specified with a number and a unit. -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct TimeAmount { /// The actual amount. pub amount: usize, @@ -172,14 +172,101 @@ pub struct Task<'a> { /// Date of creation. pub creation_date: Option, /// Description of the [`Task`]. - pub description: &'a str, + // `description` can't be public as it is closely tied to `tags`. Changing one means changing + // the other. Access and modification should be done through separate methods. + description: &'a str, /// Associated [`Tag`]s. /// /// For a list of supported tags, see [`Tag`]. - pub tags: Vec>, + // `tags` can't be public as it is closely tied to `description`. Changing one means changing + // the other. Access and modification should be done through separate methods. + tags: Vec>, } impl<'a> Task<'a> { + /// Create a new task. + /// + /// The tags of the task are parsed from the description. As the tags and the description are + /// closely tied together, one changing if the other does, mutable access is only granted to + /// the description, which, when changed, changes the tags, too. + /// + /// ```rust + /// # use todotxt_parser::{Completion, Tag, Task, TodoError}; + /// # use chrono::NaiveDate; + /// let raw_task = "x (A) 2016-05-20 2016-04-30 measure space for +chapelShelving @chapel due:2016-05-30"; + /// let task = Task::try_from(raw_task)?; + /// assert_eq!(task.tags(), &vec![ + /// Tag::Project("chapelShelving"), + /// Tag::Context("chapel"), + /// Tag::Due(NaiveDate::from_ymd_opt(2016, 5, 30).unwrap()) + /// ]); + /// # Ok::<(), TodoError>(()) + /// ``` + #[must_use] + pub fn new( + completion: Completion, + priority: u8, + creation_date: Option, + description: &'a str, + ) -> Self { + let tags = parser::description_tags(description) + .map(|(_, res)| res) + .unwrap_or_default(); + + Self { + completion, + priority, + creation_date, + description, + tags, + } + } + + #[must_use] + pub const fn completion(&self) -> &Completion { + &self.completion + } + + pub fn set_completion(&mut self, completion: Completion) { + self.completion = completion; + } + + #[must_use] + pub const fn priority(&self) -> u8 { + self.priority + } + + pub fn set_priority(&mut self, priority: u8) { + self.priority = priority; + } + + #[must_use] + pub const fn creation_date(&self) -> Option { + self.creation_date + } + + pub fn set_creation_date(&mut self, creation_date: Option) { + self.creation_date = creation_date; + } + + #[must_use] + pub const fn description(&self) -> &'a str { + self.description + } + + pub fn set_description(&mut self, description: &'a str) { + let tags = parser::description_tags(description) + .map(|(_, res)| res) + .unwrap_or_default(); + self.description = description; + self.tags = tags; + } + + #[must_use] + pub const fn tags(&self) -> &Vec { + &self.tags + } + /// Return the projects found in the description of the task. #[must_use] pub fn projects(&self) -> Vec<&'a str> { @@ -212,11 +299,19 @@ impl<'a> Task<'a> { /// Return the due date of the task, if there is one. #[must_use] - pub fn due(&self) -> Option { + pub fn due(&self) -> Option<&NaiveDate> { self.tags .iter() - .find_map(|t| if let Tag::Due(d) = t { Some(*d) } else { None }) + .find_map(|t| if let Tag::Due(d) = t { Some(d) } else { None }) } + + /// Return the recurring rule of the task, if there is one. + #[must_use] + pub fn rec(&self) -> Option<&Recurring> { + self.tags + .iter() + .find_map(|t| if let Tag::Rec(r) = t { Some(r) } else { None }) + } } impl<'a> @@ -321,4 +416,24 @@ mod test { Err(TodoError::IncorrectFormat(_)) )); } + + #[test] + fn test_rec() { + let rec = Recurring { + strict: false, + amount: TimeAmount { + amount: 1, + unit: TimeUnit::Year, + }, + }; + let task = Task { + completion: Completion::Incomplete, + priority: 0, + creation_date: None, + description: "sample task +project rec:1y", + tags: vec![Tag::Project("project"), Tag::Rec(rec.clone())], + }; + + assert_eq!(task.rec(), Some(&rec)); + } } blob - ee256ea0a92d53e6d320c4cda40e76b284bd50cf blob + 33bed8d1a6b05432b25add2ec1484285a2d4aa09 --- tests/test.rs +++ tests/test.rs @@ -12,22 +12,23 @@ fn test_parse_from_str_oneline() { let input = "2023-01-03 write tests for todotxt-parser +todotxt-parser @workstation due:2023-01-04"; let ts = parse_from_str(&input).unwrap(); + let tags1 = vec![ + Tag::Project("todotxt-parser"), + Tag::Context("workstation"), + Tag::Due(NaiveDate::from_ymd_opt(2023, 1, 4).unwrap()), + ]; + let tags = vec![&tags1]; assert_eq!( ts, - vec![Task { - completion: Completion::Incomplete, - priority: 0, - creation_date: Some(NaiveDate::from_ymd_opt(2023, 1, 3).unwrap()), - description: - "write tests for todotxt-parser +todotxt-parser @workstation due:2023-01-04", - tags: vec![ - Tag::Project("todotxt-parser"), - Tag::Context("workstation"), - Tag::Due(NaiveDate::from_ymd_opt(2023, 1, 4).unwrap()), - ], - }] + vec![Task::new( + Completion::Incomplete, + 0, + Some(NaiveDate::from_ymd_opt(2023, 1, 3).unwrap()), + "write tests for todotxt-parser +todotxt-parser @workstation due:2023-01-04" + )] ); + assert_eq!(ts.iter().map(Task::tags).collect::>(), tags); } #[test] @@ -36,35 +37,34 @@ fn test_parse_from_str() { "2023-01-03 write tests for todotxt-parser +todotxt-parser @workstation due:2023-01-04\n\ x 2023-01-02 grocery shopping +groceries @supermarket due:2023-01-03"; let ts = parse_from_str(input).unwrap(); + let tags1 = vec![ + Tag::Project("todotxt-parser"), + Tag::Context("workstation"), + Tag::Due(NaiveDate::from_ymd_opt(2023, 1, 4).unwrap()), + ]; + let tags2 = vec![ + Tag::Project("groceries"), + Tag::Context("supermarket"), + Tag::Due(NaiveDate::from_ymd_opt(2023, 1, 3).unwrap()), + ]; + let tags = vec![&tags1, &tags2]; assert_eq!( ts, vec![ - Task { - completion: Completion::Incomplete, - priority: 0, - creation_date: Some(NaiveDate::from_ymd_opt(2023, 1, 3).unwrap()), - description: - "write tests for todotxt-parser +todotxt-parser @workstation due:2023-01-04", - tags: vec![ - Tag::Project("todotxt-parser"), - Tag::Context("workstation"), - Tag::Due(NaiveDate::from_ymd_opt(2023, 1, 4).unwrap()), - ], - }, - Task { - completion: Completion::Complete(Some( - NaiveDate::from_ymd_opt(2023, 1, 2).unwrap() - )), - priority: 0, - creation_date: None, - description: "grocery shopping +groceries @supermarket due:2023-01-03", - tags: vec![ - Tag::Project("groceries"), - Tag::Context("supermarket"), - Tag::Due(NaiveDate::from_ymd_opt(2023, 1, 3).unwrap()) - ], - }, + Task::new( + Completion::Incomplete, + 0, + Some(NaiveDate::from_ymd_opt(2023, 1, 3).unwrap()), + "write tests for todotxt-parser +todotxt-parser @workstation due:2023-01-04" + ), + Task::new( + Completion::Complete(Some(NaiveDate::from_ymd_opt(2023, 1, 2).unwrap())), + 0, + None, + "grocery shopping +groceries @supermarket due:2023-01-03" + ), ] ); + assert_eq!(ts.iter().map(Task::tags).collect::>(), tags); } blob - 4c7155c585e67c7ae402985b0a31e3cb069b1e45 blob + 69a61ac9e58924aa1b196262ccfc2ae3fdb240cd --- tests/types.rs +++ tests/types.rs @@ -13,18 +13,19 @@ const TASK_STR: &str = #[test] fn test_task_try_from_str() { let res = Task::try_from(TASK_STR).unwrap(); + let tags = vec![ + Tag::Project("chapelShelving"), + Tag::Context("chapel"), + Tag::Due(NaiveDate::from_ymd_opt(2016, 5, 30).unwrap()), + ]; assert_eq!( res, - Task { - completion: Completion::Complete(Some(NaiveDate::from_ymd_opt(2016, 5, 20).unwrap())), - priority: 1, - creation_date: Some(NaiveDate::from_ymd_opt(2016, 4, 30).unwrap()), - description: "measure space for +chapelShelving @chapel due:2016-05-30", - tags: vec![ - Tag::Project("chapelShelving"), - Tag::Context("chapel"), - Tag::Due(NaiveDate::from_ymd_opt(2016, 5, 30).unwrap()) - ] - } + Task::new( + Completion::Complete(Some(NaiveDate::from_ymd_opt(2016, 5, 20).unwrap())), + 1, + Some(NaiveDate::from_ymd_opt(2016, 4, 30).unwrap()), + "measure space for +chapelShelving @chapel due:2016-05-30" + ) ); + assert_eq!(res.tags(), &tags); }