use anyhow::{anyhow, Result};
use regex::{escape, Regex};
pub mod config;
pub struct Project {
pub name: String,
pub current_task: Task,
pub todo: u8,
pub done: u8,
pub tasks: Vec<String>,
}
#[derive(Debug)]
pub struct Task {
pub priority: Option<char>,
pub description: String,
pub project: String,
pub context: String,
}
impl Task {
fn new(task_line: &str) -> Result<Self> {
match validate_task_line(task_line) {
Ok(re) => {
let caps = match re.captures(task_line) {
Some(caps) => caps,
None => return Err(anyhow!("The regex is already matched during validation, this should not be happening!\ntask_line: {:?}\nre: {:?}", task_line, re)),
};
let priority = match caps.name("priority") {
Some(prio) => {
let prio: Vec<_> = prio.as_str().to_string().chars().collect();
Some(prio[0])
}
None => None,
};
let description = match caps.name("description") {
Some(desc) => desc.as_str().trim().to_string(),
None => "".to_string(),
};
let project = match caps.name("project") {
Some(proj) => proj.as_str().trim().to_string(),
None => "".to_string(),
};
let context = match caps.name("context") {
Some(cont) => cont.as_str().trim().to_string(),
None => "".to_string(),
};
Ok(Self {
priority,
description,
project,
context,
})
}
Err(err) => {
eprintln!(
"could not validate task_line: {}\nError: {}",
task_line, err
);
Ok(Self {
priority: None,
description: task_line.to_string(),
project: "".to_string(),
context: "".to_string(),
})
}
}
}
}
fn validate_task_line(task_line: &str) -> Result<Regex, String> {
let re1 = match Regex::new(
r"^([\(](?<priority>[A-Z])[\)])?(?<description>[[:alnum:]\s_\-\.']+)+(\+(?<project>[[:alnum:]]+))?\s?(@(?<context>[[:alnum:]]+))?$",
) {
Ok(re) => re,
Err(err) => return Err(format!("Issue with pattern: {}", err).to_string()),
};
let re2 = match Regex::new(
r"^([\(](?<priority>[A-Z])[\)])?(?<description>[[:alnum:]\s_\-\.']+)+(@(?<context>[[:alnum:]]+))?\s?(\+(?<project>[[:alnum:]]+))?$",
) {
Ok(re) => re,
Err(err) => return Err(format!("Issue with pattern: {}", err).to_string()),
};
if re1.is_match(task_line) {
Ok(re1)
} else if re2.is_match(task_line) {
Ok(re2)
} else {
Err("Invalid task line.".to_string())
}
}
pub fn get_project(name: String, todos: &str, dones: &str) -> Result<Option<Project>> {
let pattern = escape(&name);
let pattern = String::from(r"\+") + &pattern + r"\b";
let re = match Regex::new(&pattern) {
Ok(re) => re,
Err(err) => {
eprintln!("could not use pattern: {}\nError: {}", &pattern, err);
return Ok(None);
}
};
if re.is_match(todos) || re.is_match(dones) {
let mut todo_matches: Vec<_> = todos.lines().filter(|l| re.is_match(l)).collect();
let done_count = re.find_iter(dones).count();
todo_matches.sort();
Ok(Some(Project {
name,
current_task: Task::new(todo_matches[0])?,
todo: todo_matches.len() as u8,
done: done_count as u8,
tasks: todo_matches.iter().map(|m| m.to_string()).collect(),
}))
} else {
Ok(None)
}
}
pub fn get_top_task(todos: &str) -> Result<Option<Task>> {
let mut todos: Vec<_> = todos.lines().collect();
if todos.is_empty() {
Ok(None)
} else {
todos.sort();
let task_line = todos[0];
Ok(Some(Task::new(task_line)?))
}
}
#[cfg(test)]
mod tests {
use super::*;
const TEST_DATA: &str = r"
(C) do not match +testproj
(D) match @context +projtest
(B) match this one +projtest @textcon
(D) do not match +testproj
";
const DONE_DATA: &str = r"
(E) do not match +testproj
(A) match done @context +projtest
(C) match done +projtest @textcon
(B) match done +projtest @textcon
(F) do not match +testproj
";
const VALID_TASKS: &str = r"(A) A description's _fine_ example-sentence. +alph2a @context
(B) description 2 @context +alph2a
(C) description 3 +alph2a
(D) description 4 @context
(E) description 5
description 6 +alph2a @context
(F) description 7 @context +alph2a
description 8 +alph2a
description 9 @context
description 10";
const INVALID_TASKS: &str = r"(b) description 1 @context +alph2a
(C) description 2 +alph2a desc
() description & ? ! , ; 3 @context
(3) description 4
description 5 +alph2a description @context
description 6 +alph2a description
description @ 7
description + 8";
#[test]
fn test_get_project() {
if let Some(project) = get_project("projtest".to_string(), TEST_DATA, DONE_DATA).unwrap() {
assert_eq!(project.name, "projtest");
assert_eq!(project.current_task.description, "match this one");
assert_eq!(project.todo, 2);
assert_eq!(project.done, 3);
}
}
#[test]
fn test_get_no_project() {
assert!(get_project("absent".to_string(), TEST_DATA, DONE_DATA)
.unwrap()
.is_none());
}
#[test]
fn test_valid_tasks() {
for line in VALID_TASKS.lines() {
assert!(validate_task_line(line).is_ok());
}
}
#[test]
fn test_valid_top_task() {
let result = get_top_task(VALID_TASKS).unwrap().unwrap();
assert_eq!(result.priority.unwrap(), 'A');
assert_eq!(
result.description,
"A description's _fine_ example-sentence."
);
assert_eq!(result.project, "alph2a");
assert_eq!(result.context, "context");
}
#[test]
fn test_invalid_top_task() {
let result = get_top_task(INVALID_TASKS).unwrap().unwrap();
assert_eq!(result.priority, None);
assert_eq!(result.description, "() description & ? ! , ; 3 @context");
assert_eq!(result.project, "");
assert_eq!(result.context, "");
}
#[test]
fn test_empty_todo() {
assert!(get_top_task("").unwrap().is_none());
}
#[test]
fn test_invalid_tasks() {
for line in INVALID_TASKS.lines() {
assert!(validate_task_line(line).is_err());
}
}
}