taskforge 0.2.0

Task management shared functions and structures for the taskforge family of tools.
Documentation
// Copyright 2018 Mathew Robinson <chasinglogic@gmail.com>. All rights reserved. Use of this source code is
// governed by the Apache-2.0 license that can be found in the LICENSE file.

//! The task module provides the Task and Note structs which are
//! central types in taskforge.
use bson::oid::ObjectId;
use chrono::prelude::*;
use serde_json;

use std::cmp::{Ord, Ordering, PartialOrd};
use std::fmt;
use std::str;

/// Task is the primary data object in taskforge everything works on
/// or around Tasks.  All tasks have an `id` field which is a unique
/// identifier generated as a BSON ObjectID converted to a hex string.
///
/// All fields of a task are public however you should not generally
/// change the id of an existing Task.
///
/// All tasks implement Serialize and Deserialize from serde.
///
/// Additionally Tasks implement Eq, PartialEq, and Ord so given a
/// container of tasks that allows for sorting one can sort the Tasks
/// by priority descending followed by oldest created date first.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct Task {
    pub id: String,
    pub title: String,
    pub context: String,
    pub created_date: DateTime<Local>,
    pub priority: i64,
    pub notes: Vec<Note>,
    pub completed_date: Option<DateTime<Local>>,
    pub body: Option<String>,
}

impl Task {
    /// Create a new `Task` with `title`.
    ///
    /// This function will auto generate a BSON ObjectID as well as
    /// set other fields to their default values.
    ///
    /// The Default values are as follows:
    ///
    /// | Field           | Type                              | Default Value                                          |
    /// |-----------------|-----------------------------------|--------------------------------------------------------|
    /// | id              | `String`                          | BSON ObjectID                                          |
    /// | context         | `String`                          | `"default"`                                            |
    /// | created\_date   | `Chrono::DateTime<Local>`         | current local time created with `Chrono::Local::now()` |
    /// | completed\_date | `Option<Chrono::DateTime<Local>>` | `None`                                                 |
    /// | priority        | `f64`                             | `1.0`                                                  |
    /// | notes           | `Vec<Note>`                       | `[]`                                                   |
    /// | body            | `String`                          | `""`                                                   |
    ///
    pub fn new(title: &str) -> Task {
        Task {
            id: ObjectId::new().unwrap().to_hex(),
            title: title.to_string(),
            context: "default".to_string(),
            created_date: Local::now(),
            priority: 1,
            completed_date: None,
            body: None,
            notes: Vec::new(),
        }
    }

    /// Change the context of a task during creation using the builder
    /// pattern.
    pub fn with_context(mut self, context: &str) -> Task {
        self.context = context.to_string();
        self
    }

    /// Change the priority of a task during creation using the builder
    /// pattern.
    pub fn with_priority(mut self, priority: i64) -> Task {
        self.priority = priority;
        self
    }

    /// Change the body of a task during creation using the builder
    /// pattern.
    pub fn with_body(mut self, body: &str) -> Task {
        self.body = Some(body.to_string());
        self
    }

    /// Complete a task during creation using the builder pattern.
    pub fn precomplete(mut self) -> Task {
        self.complete();
        self
    }

    /// Complete this task
    pub fn complete(&mut self) {
        self.completed_date = Some(Local::now());
    }

    /// Returns a boolean indicating whether this task is completed or
    /// not.
    pub fn completed(&self) -> bool {
        match self.completed_date {
            Some(_) => true,
            None => false,
        }
    }

    /// Add a note to the task with `message`
    pub fn add_note(&mut self, message: &str) {
        self.notes.push(Note::new(message));
    }

    /// Serialize this task to JSON
    pub fn to_json(&self) -> Result<String, serde_json::Error> {
        serde_json::to_string_pretty(self)
    }

    /// Update this task with all fields from `other` except for
    /// `self.id`
    pub fn update(&mut self, mut other: Task) {
        self.title = other.title;
        self.context = other.context;
        self.priority = other.priority;
        self.completed_date = other.completed_date;
        self.body = other.body;
        self.notes.append(&mut other.notes);
    }
}

impl fmt::Display for Task {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match serde_json::to_string_pretty(self) {
            Ok(jsn) => write!(f, "{}", jsn),
            Err(_) => write!(f, "{}", self.title),
        }
    }
}

impl Eq for Task {}

impl Ord for Task {
    fn cmp(&self, other: &Task) -> Ordering {
        match self.partial_cmp(other) {
            Some(order) => order,
            None => Ordering::Less,
        }
    }
}

impl PartialOrd for Task {
    fn partial_cmp(&self, other: &Task) -> Option<Ordering> {
        match self.priority.partial_cmp(&other.priority) {
            Some(Ordering::Equal) => Some(self.created_date.date().cmp(&other.created_date.date())),
            Some(order) => Some(order.reverse()),
            None => None,
        }
    }
}

/// Note is a comment or additional information on a Task added after the fact.
/// Notes, like Tasks, use BSON ObjectIDs as Strings for their id field.
///
/// To create a Note use:
///
/// ```
/// use taskforge::task::Note;
///
/// let note = Note::new("this is the body of my note");
/// ```
///
/// This will automatically add the `created_date` and `id` fields to the note.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct Note {
    pub id: String,
    pub created_date: DateTime<Local>,
    pub body: String,
}

impl Note {
    pub fn new(body: &str) -> Note {
        Note {
            id: ObjectId::new().unwrap().to_hex(),
            created_date: Local::now(),
            body: body.to_string(),
        }
    }
}

impl fmt::Display for Note {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match serde_json::to_string_pretty(self) {
            Ok(jsn) => write!(f, "{}", jsn),
            Err(_) => write!(f, "{}", self.body),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use chrono::Duration;

    #[test]
    fn test_new_task() {
        let task = Task::new("Some title");
        assert!(task.title == "Some title".to_string());
        assert!(task.completed_date.is_none());
        assert!(!task.completed());
    }

    #[test]
    fn test_complete_task() {
        let mut task = Task::new("Test");
        task.complete();
        assert!(task.completed_date.is_some());
        assert!(task.completed());
    }

    #[test]
    fn test_simple_task_ordering() {
        let mut list = vec![
            Task::new("test1").with_priority(1),
            Task::new("test3").with_priority(3),
            Task::new("test2").with_priority(2),
            Task::new("test0").with_priority(0),
        ];

        list.sort();

        let mut priority = 3;
        for task in list {
            println!("Task: {}", task.title);
            assert_eq!(task.priority, priority);
            priority = priority - 1;
        }
    }

    #[test]
    fn test_multi_day_task_ordering() {
        let mut yesterday = Task::new("test2").with_priority(2);
        yesterday.created_date = Local::now() - Duration::days(1);

        let tasks = vec![
            Task::new("test1").with_priority(1),
            yesterday,
            Task::new("test3").with_priority(3),
            Task::new("test0").with_priority(2),
        ];

        let mut list = tasks.clone();
        list.sort();

        let mut iter = list.into_iter();
        assert_eq!(iter.next().unwrap(), tasks[2]);
        assert_eq!(iter.next().unwrap(), tasks[1]);
        assert_eq!(iter.next().unwrap(), tasks[3]);
        assert_eq!(iter.next().unwrap(), tasks[0]);
    }

    #[test]
    fn test_unique_ids() {
        let t1 = Task::new("Test task");
        let t2 = Task::new("Test task");
        assert_ne!(t1.id, t2.id)
    }
}