leafslug_tsk 0.4.0

A personal Task Management System that is not inteded to be used by others... yet.
Documentation
use nom::{
    branch::alt,
    bytes::complete::{tag, take_while},
    character::complete::{not_line_ending, tab},
    combinator::{all_consuming, map},
    error::{context, ContextError, ParseError, VerboseError},
    sequence::{preceded, tuple},
    Finish,
};

use super::{task::Task, Error};
pub fn form_file_action(tasks: &[Task]) -> String {
    tasks
        .iter()
        .enumerate()
        .map(|(i, t)| format!("{i:03}\tignr\t{}", t.title))
        .fold(String::new(), |accu, l| format!("{accu}{l}\n"))
}

pub fn multiple_lines(input: &str) -> Result<Vec<(u64, Action)>, Error> {
    input
        .lines()
        .map(action_from_line)
        .try_fold(vec![], |mut a, x| {
            a.push(x?);
            Ok(a)
        })
}
pub fn action_from_line(input: &str) -> Result<(u64, Action), Error> {
    let (_, res) = line_parser::<VerboseError<&str>>(input)
        .finish()
        .map_err(|e| Error::ParsingLineFailed(Box::new(e.to_string())))?;
    Ok(res)
}

fn line_parser<'a, E>(input: &'a str) -> Result<(&'a str, (u64, Action)), nom::Err<E>>
where
    E: ParseError<&'a str> + ContextError<&'a str>,
{
    all_consuming(tuple((
        context(
            "parsing number at the start of the line",
            nom::character::complete::u64,
        ),
        preceded(
            context("tab before action", tab),
            context(
                "getting action",
                alt((
                    map(
                        tuple((tag("done"), tab, context("title", not_line_ending))),
                        |_| Action::Done,
                    ),
                    map(
                        preceded(
                            tuple((
                                tag("aban"),
                                tab,
                                context(
                                    "title",
                                    take_while(|x: char| !x.eq_ignore_ascii_case(&'\t')),
                                ),
                            )),
                            preceded(tab, context("reason", not_line_ending)),
                        ),
                        |reason: &'a str| Action::Abandon(Some(reason.to_owned())),
                    ),
                    map(
                        tuple((tag("aban"), tab, context("title", not_line_ending))),
                        |_| Action::Abandon(None),
                    ),
                    map(
                        tuple((
                            tag("ignr"),
                            tab,
                            context(
                                "title",
                                take_while(|x: char| !x.eq_ignore_ascii_case(&'\t')),
                            ),
                        )),
                        |_| Action::Ignore,
                    ),
                    map(
                        tuple((
                            tag("back"),
                            tab,
                            context(
                                "title",
                                take_while(|x: char| !x.eq_ignore_ascii_case(&'\t')),
                            ),
                        )),
                        |_| Action::Backlog,
                    ),
                    map(
                        tuple((
                            tag("todo"),
                            tab,
                            context(
                                "title",
                                take_while(|x: char| !x.eq_ignore_ascii_case(&'\t')),
                            ),
                        )),
                        |_| Action::ToDo,
                    ),
                )),
            ),
        ),
    )))(input)
}

#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Action {
    Ignore,
    Done,
    ToDo,
    Abandon(Option<String>),
    Backlog,
}
#[cfg(test)]
mod testing {
    #![allow(clippy::unwrap_used)]
    use crate::task::{State, StateKind};

    #[allow(clippy::wildcard_imports)]
    use super::*;
    use rstest::*;

    #[rstest]
    #[case::ignr("1\tignr\ttask description, done ", (1, Action::Ignore))]
    #[case::done("0\tdone\ttask description", (0, Action::Done))]
    #[case::todo("2\ttodo\ttask description",(2, Action::ToDo))]
    #[case::aban("10\taban\ttask description",(10, Action::Abandon(None)))]
    #[case::aban_with_reason("10\taban\ttask description\tsome reason",(10, Action::Abandon(Some("some reason".to_owned()))))]
    #[case::back("01\tback\ttask description", (1, Action::Backlog))]
    fn bulk_parser_oneline_happy(#[case] input: &str, #[case] expect: (u64, Action)) {
        assert_eq!(action_from_line(input).unwrap(), expect);
    }
    #[rstest]
    #[case::ignr("\tignr\ttask description, done\t ", (1, Action::Ignore))]
    #[case::done("done\ttask description\t", (0, Action::Done))]
    #[case::todo("2\ttodo\ttask description\t0",(2, Action::ToDo))]
    #[case::aban("10 10\taban\ttask description",(10, Action::Abandon(None)))]
    #[case::aban_with_reason("1 0\taban\ttask description\tsome reason",(10, Action::Abandon(Some("some reason".to_owned()))))]
    #[case::back("01 back ttask description", (1, Action::Backlog))]
    #[should_panic]
    fn bulk_parser_oneline_not_happy(#[case] input: &str, #[case] expect: (u64, Action)) {
        assert_eq!(action_from_line(input).unwrap(), expect);
    }
    #[rstest]
    #[case::ignr_multiple("1\tignr\ttask description, done \n0\tdone\ttask description\n10\taban\ttask description\tsome reason\n", vec![(1, Action::Ignore), (0, Action::Done),(10, Action::Abandon(Some("some reason".to_owned())))])]
    #[case::done_single("0\tdone\ttask description\n", vec![(0, Action::Done)])]
    fn bulk_parser_multiline(#[case] input: &str, #[case] expect: Vec<(u64, Action)>) {
        assert_eq!(multiple_lines(input).unwrap(), expect);
    }
    #[fixture]
    fn tasks() -> (Vec<Task>, String) {
        let now = time::OffsetDateTime::now_utc();
        let v = vec![
            Task {
                id: now.unix_timestamp(),
                time_created: now,
                state_log: vec![State::new(now, StateKind::ToDo)],
                title: "first one".to_owned(),
                description: None,
                area: None,
                people: vec![],
                projects: vec![],
                start: None,
                end: None,
            },
            Task {
                id: now.unix_timestamp(),
                time_created: now,
                state_log: vec![State::new(now, StateKind::ToDo)],
                title: "second task".to_owned(),
                description: None,
                area: None,
                people: vec![],
                projects: vec![],
                start: None,
                end: None,
            },
            Task {
                id: now.unix_timestamp(),
                time_created: now,
                state_log: vec![State::new(now, StateKind::ToDo)],
                title: "third task".to_owned(),
                description: None,
                area: None,
                people: vec![],
                projects: vec![],
                start: None,
                end: None,
            },
        ];
        (
            v,
            "000\tignr\tfirst one\n001\tignr\tsecond task\n002\tignr\tthird task\n".to_owned(),
        )
    }
    #[rstest]
    fn test_the_tasks(tasks: (Vec<Task>, String)) {
        assert_eq!(form_file_action(&tasks.0), tasks.1);
    }
}