commit a6078516315466372617a4c3b72a6b88d04ad76b from: witcher date: Fri Jan 6 16:59:55 2023 UTC Make fields of types public, strict completion All fields in all structs in `types.rs` have been made public. The `completed` field in `Tag` has been replaced with a `completion` field with type `Completion`. This encodes in the type system that a task can only ever has a date of completion if it is actually complete. commit - c6bbc32edce650be04dc40fbc37cbd56dea19819 commit + a6078516315466372617a4c3b72a6b88d04ad76b blob - af4b5dbd2ab934d82ac95ac79ff2cb7de160e0bf blob + 619b035850901ba580d8d12a32b873bdb8c815ba --- src/err.rs +++ src/err.rs @@ -13,6 +13,9 @@ pub enum TodoError { /// The input has an incorrect format. #[error("Incorrect format: {0}")] IncorrectFormat(&'static str), + /// Illegal arguments have been passed. + #[error("Illegal arguments: {0}")] + IllegalArguments(&'static str), /// An error occurred while parsing the input. #[error("Error while parsing: {0}")] ParsingError(&'static str), blob - 90d7b67f27f240c2226b38c47362f18ff5c422c6 blob + 93be29963141c72b6fc164cf97828d4c2448738c --- src/lib.rs +++ src/lib.rs @@ -24,7 +24,7 @@ mod parser; mod types; pub use err::TodoError; -pub use types::{Recurring, Tag, Task, TimeAmount, TimeUnit}; +pub use types::{Completion, Recurring, Tag, Task, TimeAmount, TimeUnit}; use nom::Finish; @@ -33,11 +33,17 @@ use nom::Finish; /// This is the main entry point of the library. /// /// ```rust -/// # use todotxt_parser::{parse_from_str, Task, TodoError}; +/// # use todotxt_parser::{Completion, 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::new(false, 0, None, Some(NaiveDate::from_ymd_opt(2023, 1, 5).unwrap()), "sample task", vec![])]); +/// 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![] +/// }]); /// # Ok::<(), TodoError>(()) /// ``` /// blob - 486f07c6fa14e952d651a804ec4d8c6ab195da37 blob + ff10e0718af6f6f74f07409dfa6045cc7afcbb4d --- src/parser.rs +++ src/parser.rs @@ -170,7 +170,7 @@ fn time_amount(i: &str) -> IResult<&str, TimeAmount> { verify(map_res(digit1, str::parse::), |d: &usize| *d > 0), time_unit, )), - |(amount, unit)| TimeAmount::new(amount, unit), + |(amount, unit)| TimeAmount { amount, unit }, )(i) } @@ -180,7 +180,7 @@ pub fn recurring(i: &str) -> IResult<&str, Recurring> map(opt(value(true, tag("+"))), |v| v.unwrap_or(false)), time_amount, )), - |(strict, amount)| Recurring::new(strict, amount), + |(strict, amount)| Recurring { strict, amount }, )(i) } @@ -240,7 +240,7 @@ pub fn tasks(i: &str) -> IResult<&str, Vec> { #[cfg(test)] mod tests { use super::*; - use crate::types::Tag; + use crate::types::{Completion, Recurring, Tag, Task, TimeAmount, TimeUnit}; use nom::error::{Error, ErrorKind}; #[test] @@ -497,10 +497,13 @@ mod tests { vec![ Tag::KeyValue(("key", "value")), Tag::Context("context"), - Tag::Rec(Recurring::new( - false, - TimeAmount::new(132049, TimeUnit::Day) - )), + Tag::Rec(Recurring { + strict: false, + amount: TimeAmount { + amount: 132049, + unit: TimeUnit::Day + } + }), Tag::Project("project"), ], )) @@ -518,18 +521,17 @@ mod tests { task("(A) 2023-01-01 a perfectly normal task +testing @home due:2023-01-02"), Ok(( "", - Task::new( - false, - 1, - None, - Some(NaiveDate::from_ymd_opt(2023, 01, 01).unwrap()), - "a perfectly normal task +testing @home due:2023-01-02", - 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()) ], - ) + } )) ); @@ -537,20 +539,19 @@ mod tests { 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::new( - true, - 0, - Some(NaiveDate::from_ymd_opt(2023, 01, 02).unwrap()), - Some(NaiveDate::from_ymd_opt(2023, 01, 01).unwrap()), - "a completed task with completion date +testing @home due:2023-01-02 @place +todotxt-parser", - vec![ + 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"), ], - ), + }, )) ); } @@ -565,33 +566,34 @@ mod tests { Ok(( "", vec![ - Task::new( - false, - 1, - None, - Some(NaiveDate::from_ymd_opt(2023, 01, 01).unwrap()), - "a perfectly normal task +testing @home due:2023-01-02", - 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::new( - true, - 0, - Some(NaiveDate::from_ymd_opt(2023, 01, 02).unwrap()), - 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", - vec![ + }, + 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::new(true, TimeAmount::new(4, TimeUnit::Month))), + Tag::Rec(Recurring { + strict: true, + amount: TimeAmount { + amount:4, unit :TimeUnit::Month}}) ], - ), + }, ], )) ); @@ -603,14 +605,26 @@ mod tests { recurring("+5m"), Ok(( "", - Recurring::new(true, TimeAmount::new(5, TimeUnit::Month)) + Recurring { + strict: true, + amount: TimeAmount { + amount: 5, + unit: TimeUnit::Month + } + } )) ); assert_eq!( recurring("64y"), Ok(( "", - Recurring::new(false, TimeAmount::new(64, TimeUnit::Year)) + Recurring { + strict: false, + amount: TimeAmount { + amount: 64, + unit: TimeUnit::Year + } + } )) ); } blob - ee842dc9b02f370b4e5c8e8126c770fbdd326916 blob + 3ad4f7ef9998a63833bd53630e40304a3edcd283 --- src/types.rs +++ src/types.rs @@ -85,19 +85,11 @@ pub struct Recurring { /// task. /// If `false`, the new task will be created an amount of time after the *completion* of the /// task. - strict: bool, + pub strict: bool, /// What interval the task should be recurring in. - amount: TimeAmount, + pub amount: TimeAmount, } -impl Recurring { - /// Create a new [`Recurring`] instance. - #[must_use] - pub const fn new(strict: bool, amount: TimeAmount) -> Self { - Self { strict, amount } - } -} - /// A unit of time applicable for the [`Recurring`] tag. #[derive(Debug, PartialEq, Eq)] pub enum TimeUnit { @@ -129,96 +121,65 @@ impl TryFrom for TimeUnit { #[derive(Debug, PartialEq, Eq)] pub struct TimeAmount { /// The actual amount. - amount: usize, + pub amount: usize, /// The unit. - unit: TimeUnit, + pub unit: TimeUnit, } -impl TimeAmount { - /// Create a new [`TimeAmount`] instance. - #[must_use] - pub const fn new(amount: usize, unit: TimeUnit) -> Self { - Self { amount, unit } +/// The completion state of a [`Task`]. +/// +/// A task can either be [`Incomplete`] or [`Complete`]. Should the task be complete, there can +/// optionally be a completion date associated with it, marking the date of completion. +/// +/// [`Incomplete`]: Self::Incomplete +/// [`Complete`]: Self::Complete +#[derive(Debug, PartialEq, Eq)] +pub enum Completion { + Incomplete, + Complete(Option), +} + +impl TryFrom<(bool, Option)> for Completion { + type Error = TodoError; + + fn try_from( + (completed, completion_date): (bool, Option), + ) -> Result { + if !completed && completion_date.is_some() { + return Err(TodoError::IllegalArguments( + "Received completion date, but task is incomplete", + )); + } + + Ok(if completed { + Self::Complete(completion_date) + } else { + Self::Incomplete + }) } } /// A single todo.txt task. -#[derive(Debug, Default, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq)] pub struct Task<'a> { - /// Whether the task has been completed or not. - completed: bool, - /// What priority the task is associated with. + /// Whether the [`Task`] has been completed or not. + pub completion: Completion, + /// What priority the [`Task`] is associated with. /// - /// A priority of 0 indicates no priority. Possible priority values are 1-26, corresponding to - /// letters A-Z. - priority: u8, - /// Date of completion. - completion_date: Option, + /// A priority of 0 indicates no priority. Possible priority values are in range `1..=26`, + /// corresponding to letters in range `A..=Z`. + pub priority: u8, /// Date of creation. - creation_date: Option, - /// Description of the task. - description: &'a str, - /// Tags associated with the task. + pub creation_date: Option, + /// Description of the [`Task`]. + pub description: &'a str, + /// Associated [`Tag`]s. /// /// For a list of supported tags, see [`Tag`]. - tags: Vec>, + pub tags: Vec>, } impl<'a> Task<'a> { - /// Create a new [`Task`]. - #[must_use] - pub const fn new( - completed: bool, - priority: u8, - completion_date: Option, - creation_date: Option, - description: &'a str, - tags: Vec>, - ) -> Self { - Self { - completed, - priority, - completion_date, - creation_date, - description, - tags, - } - } - - /// Return whether the task has been completed yet or not. - #[must_use] - pub const fn completed(&self) -> bool { - self.completed - } - - /// Return the priority of a task. A priority of `0` means no priority has been set. - #[must_use] - pub const fn priority(&self) -> u8 { - self.priority - } - - /// Return the completion date of the task, if there is one. - /// - /// This function should always return [`None`] if the task has not been completed yet. - #[must_use] - pub const fn completion_date(&self) -> Option { - debug_assert!(!self.completed && self.completion_date.is_none()); - - self.completion_date - } - - /// Return the creation date of the task, if there is one. - #[must_use] - pub const fn creation_date(&self) -> Option { - self.creation_date - } - - /// Return the description of the task. - #[must_use] - pub const fn description(&self) -> &'a str { - self.description - } - /// Return the projects found in the description of the task. #[must_use] pub fn projects(&self) -> Vec<&'a str> { @@ -310,9 +271,8 @@ impl<'a> .1; Ok(Task { - completed, + completion: (completed, completion_date).try_into()?, priority, - completion_date, creation_date, description, tags, @@ -348,10 +308,13 @@ mod test { assert_eq!( Tag::new_key_value("rec", "+420y"), - Ok(Tag::Rec(Recurring::new( - true, - TimeAmount::new(420, TimeUnit::Year) - ))), + Ok(Tag::Rec(Recurring { + strict: true, + amount: TimeAmount { + amount: 420, + unit: TimeUnit::Year + } + })), ); assert!(matches!( Tag::new_key_value("rec", "0d"), blob - 56454eb4cdb425ac8c11862e0ba2d709eaa78570 blob + ee256ea0a92d53e6d320c4cda40e76b284bd50cf --- tests/test.rs +++ tests/test.rs @@ -5,7 +5,7 @@ */ use chrono::NaiveDate; -use todotxt_parser::{parse_from_str, Tag, Task}; +use todotxt_parser::{parse_from_str, Completion, Tag, Task}; #[test] fn test_parse_from_str_oneline() { @@ -15,18 +15,18 @@ fn test_parse_from_str_oneline() { assert_eq!( ts, - vec![Task::new( - false, - 0, - None, - Some(NaiveDate::from_ymd_opt(2023, 1, 3).unwrap()), - "write tests for todotxt-parser +todotxt-parser @workstation due:2023-01-04", - vec![ + 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()), ], - )] + }] ); } @@ -40,30 +40,31 @@ fn test_parse_from_str() { assert_eq!( ts, vec![ - Task::new( - false, - 0, - None, - Some(NaiveDate::from_ymd_opt(2023, 1, 3).unwrap()), - "write tests for todotxt-parser +todotxt-parser @workstation due:2023-01-04", - 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::new( - true, - 0, - Some(NaiveDate::from_ymd_opt(2023, 1, 2).unwrap()), - None, - "grocery shopping +groceries @supermarket due:2023-01-03", - vec![ + }, + 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()) ], - ), + }, ] ); } blob - bb99f5509528e4e497358f9312df7a5e236d0c59 blob + 4c7155c585e67c7ae402985b0a31e3cb069b1e45 --- tests/types.rs +++ tests/types.rs @@ -5,7 +5,7 @@ */ use chrono::NaiveDate; -use todotxt_parser::{Tag, Task}; +use todotxt_parser::{Completion, Tag, Task}; const TASK_STR: &str = "x (A) 2016-05-20 2016-04-30 measure space for +chapelShelving @chapel due:2016-05-30"; @@ -15,17 +15,16 @@ fn test_task_try_from_str() { let res = Task::try_from(TASK_STR).unwrap(); assert_eq!( res, - Task::new( - true, - 1, - Some(NaiveDate::from_ymd_opt(2016, 5, 20).unwrap()), - Some(NaiveDate::from_ymd_opt(2016, 4, 30).unwrap()), - "measure space for +chapelShelving @chapel due:2016-05-30", - vec![ + 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()) ] - ) + } ); }