Commit Diff


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<Tag>>>(), 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<char> 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<NaiveDate>,
     /// 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<Tag<'a>>,
+    // `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<Tag<'a>>,
 }
 
 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<NaiveDate>,
+        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<NaiveDate> {
+        self.creation_date
+    }
+
+    pub fn set_creation_date(&mut self, creation_date: Option<NaiveDate>) {
+        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<Tag> {
+        &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<NaiveDate> {
+    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::<Vec<_>>(), 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::<Vec<_>>(), 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);
 }