done 0.0.0-reserve

Personal task manager focused on sharing what you've done.
Documentation
use std::cmp::Ordering;
use std::error::Error;
use std::fmt;

#[derive(Debug,PartialEq)]
pub enum TaskError {
    InvalidState(char),
    Malformed,
}

impl fmt::Display for TaskError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let description = self.description();

        match *self {
            TaskError::InvalidState(token) => write!(
                f,
                "{}: {}",
                description,
                token),
            TaskError::Malformed => write!(
                f,
                "{}",
                description),
        }
    }
}

impl Error for TaskError {
    fn description(&self) -> &str {
        match *self {
            TaskError::InvalidState(_) => "Invalid token for task state",
            TaskError::Malformed => "Malformed definition",
        }
    }

    fn cause(&self) -> Option<&Error> {
        match *self {
            TaskError::InvalidState(_) => None,
            TaskError::Malformed => None,
        }
    }
}

#[derive(Debug,Eq,PartialEq,PartialOrd)]
pub enum TaskState {
    Current,
    Backlog,
    Blocked,
    Done,
}

impl TaskState {
    pub fn from_char(c: char) -> Result<TaskState, TaskError> {
        match c {
            '~' => Result::Ok(TaskState::Current),
            '-' => Result::Ok(TaskState::Backlog),
            '=' => Result::Ok(TaskState::Blocked),
            '+' => Result::Ok(TaskState::Done),
            token @ _ => Result::Err(TaskError::InvalidState(token))
        }
    }

    pub fn to_char(&self) -> char {
        match *self {
            TaskState::Current => '~',
            TaskState::Backlog => '-',
            TaskState::Blocked => '=',
            TaskState::Done    => '+',
        }
    }

    fn order_value(&self) -> u8 {
        match *self {
            TaskState::Current => 0,
            TaskState::Backlog => 1,
            TaskState::Blocked => 2,
            TaskState::Done    => 3,
        }
    }
}

impl Ord for TaskState {
    fn cmp(&self, other: &TaskState) -> Ordering {
        self.order_value().cmp(&other.order_value())
    }
}

#[derive(Debug,Eq,PartialEq,PartialOrd)]
pub struct Task {
    pub name: String,
    pub state: TaskState,
}

impl Task {
    pub fn new(task_str: &str) -> Result<Task, TaskError> {
        let mut lines = task_str.lines();
        if lines.clone().count() == 0 {
            return Result::Err(TaskError::Malformed)
        }

        // First line should always be the task name
        let main_line = lines.next().unwrap();
        let mut main_line_chars = main_line.chars();

        // First character must be symbol for state
        let state = try!(TaskState::from_char(main_line_chars.next().unwrap()));

        let mut passed_leading_whitespace = false;
        let mut name = String::new();

        for next_char in main_line_chars {
            // Skip the leading whitespace
            if !passed_leading_whitespace {
                if next_char.is_whitespace() {
                    continue;
                } else {
                    passed_leading_whitespace = true;
                }
            }

            name.push(next_char)
        }

        Result::Ok(Task {
            name: name,
            state: state,
        })
    }

    pub fn to_plaintext(&self) -> String {
       format!("{} {}", self.state.to_char(), self.name)
    }
}

impl fmt::Display for Task {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Task(state: {}, name: {})", self.state.to_char(), self.name)
    }
}

impl Ord for Task {
    fn cmp(&self, other: &Task) -> Ordering {
        let state_order = self.state.cmp(&other.state);

        match state_order {
            Ordering::Equal => self.name.cmp(&other.name),
            _ => state_order,
        }
    }
}

#[cfg(test)]
mod tests {
    use std::cmp::Ordering;
    use std::collections::HashMap;
    use super::*;

    #[test]
    fn test_task_state_to_char() {
        assert_eq!(TaskState::Current.to_char(), '~');
        assert_eq!(TaskState::Backlog.to_char(), '-');
        assert_eq!(TaskState::Blocked.to_char(), '=');
        assert_eq!(TaskState::Done.to_char(), '+');
    }

    #[test]
    fn test_task_state_from_char() {
        assert_eq!(TaskState::from_char('~').unwrap(), TaskState::Current);
        assert_eq!(TaskState::from_char('-').unwrap(), TaskState::Backlog);
        assert_eq!(TaskState::from_char('=').unwrap(), TaskState::Blocked);
        assert_eq!(TaskState::from_char('+').unwrap(), TaskState::Done);
        assert!(TaskState::from_char('!').is_err());
    }

    #[test]
    fn test_task_state_ord() {
        assert_eq!(TaskState::Current.cmp(&TaskState::Backlog), Ordering::Less);
        assert_eq!(TaskState::Backlog.cmp(&TaskState::Blocked), Ordering::Less);
        assert_eq!(TaskState::Blocked.cmp(&TaskState::Done), Ordering::Less);
    }

    #[test]
    fn test_task_new_states() {
        let mut valid_tasks = HashMap::new();
        valid_tasks.insert("~ current task", Task {
            name: "current task".to_string(),
            state: TaskState::Current,
        });
        valid_tasks.insert("- backlog task", Task {
            name: "backlog task".to_string(),
            state: TaskState::Backlog,
        });
        valid_tasks.insert("= blocked task", Task {
            name: "blocked task".to_string(),
            state: TaskState::Blocked,
        });
        valid_tasks.insert("+ done task", Task {
            name: "done task".to_string(),
            state: TaskState::Done,
        });

        for (task_str, task) in valid_tasks {
            assert_eq!(Task::new(task_str).unwrap(), task);
        }

        let mut invalid_tasks = HashMap::new();
        invalid_tasks.insert("! task name", TaskError::InvalidState('!'));
        invalid_tasks.insert("", TaskError::Malformed);

        for (task_str, task) in invalid_tasks {
            assert_eq!(Task::new(task_str).err().unwrap(), task);
        }
    }

    #[test]
    fn test_task_to_plaintext() {
        let task = Task {
            name: "task name".to_string(),
            state: TaskState::Current,
        };

        assert_eq!(task.to_plaintext(), "~ task name");
    }

    #[test]
    fn test_task_ord() {
        let current_task = Task { name: "current".to_string(), state: TaskState::Current };
        let backlog_task = Task { name: "backlog".to_string(), state: TaskState::Backlog };
        let blocked_task = Task { name: "blocked".to_string(), state: TaskState::Blocked };
        let done_task = Task { name: "done".to_string(), state: TaskState::Done };

        assert_eq!(current_task.cmp(&backlog_task), Ordering::Less);
        assert_eq!(backlog_task.cmp(&blocked_task), Ordering::Less);
        assert_eq!(blocked_task.cmp(&done_task), Ordering::Less);
    }
}