parse all defined fields in tasks

This commit is contained in:
Dustin J. Mitchell
2018-11-10 19:34:23 -05:00
parent de5e4e134c
commit 6a66b7a84b
8 changed files with 298 additions and 431 deletions

View File

@@ -1,3 +1,6 @@
extern crate chrono;
extern crate uuid;
mod tdb2;
mod task;

View File

@@ -1,17 +0,0 @@
use std::collections::HashMap;
#[derive(Debug)]
pub struct Task {
data: HashMap<String, String>,
}
impl Task {
/// Construct a Task from a hashmap containing named properties
pub fn from_data(data: HashMap<String, String>) -> Self {
Self { data }
}
pub fn description(&self) -> &str {
self.data.get("description").unwrap()
}
}

7
src/task/mod.rs Normal file
View File

@@ -0,0 +1,7 @@
mod task;
mod taskbuilder;
pub use self::taskbuilder::TaskBuilder;
pub use self::task::{Task, Priority, Status, Timestamp, Annotation};
pub use self::task::Priority::*;
pub use self::task::Status::*;

55
src/task/task.rs Normal file
View File

@@ -0,0 +1,55 @@
use std::collections::HashMap;
use uuid::Uuid;
use chrono::prelude::*;
pub type Timestamp = DateTime<Utc>;
#[derive(Debug, PartialEq)]
pub enum Priority {
L,
M,
H,
}
#[derive(Debug, PartialEq)]
pub enum Status {
Pending,
Completed,
Deleted,
Recurring,
Waiting,
}
#[derive(Debug, PartialEq)]
pub struct Annotation {
pub entry: Timestamp,
pub description: String,
}
/// A task, the fundamental business object of this tool.
///
/// This structure is based on https://taskwarrior.org/docs/design/task.html
#[derive(Debug)]
pub struct Task {
pub status: Status,
pub uuid: Uuid,
pub entry: Timestamp,
pub description: String,
pub start: Option<Timestamp>,
pub end: Option<Timestamp>,
pub due: Option<Timestamp>,
pub until: Option<Timestamp>,
pub wait: Option<Timestamp>,
pub modified: Timestamp,
pub scheduled: Option<Timestamp>,
pub recur: Option<String>,
pub mask: Option<String>,
pub imask: Option<u64>,
pub parent: Option<Uuid>,
pub project: Option<String>,
pub priority: Option<Priority>,
pub depends: Vec<Uuid>,
pub tags: Vec<String>,
pub annotations: Vec<Annotation>,
pub udas: HashMap<String, String>,
}

194
src/task/taskbuilder.rs Normal file
View File

@@ -0,0 +1,194 @@
use std::collections::HashMap;
use chrono::prelude::*;
use std::str;
use uuid::Uuid;
use task::{Task, Priority, Status, Timestamp, Annotation};
#[derive(Default)]
pub struct TaskBuilder {
status: Option<Status>,
uuid: Option<Uuid>,
entry: Option<Timestamp>,
description: Option<String>,
start: Option<Timestamp>,
end: Option<Timestamp>,
due: Option<Timestamp>,
until: Option<Timestamp>,
wait: Option<Timestamp>,
modified: Option<Timestamp>,
scheduled: Option<Timestamp>,
recur: Option<String>,
mask: Option<String>,
imask: Option<u64>,
parent: Option<Uuid>,
project: Option<String>,
priority: Option<Priority>,
depends: Vec<Uuid>,
tags: Vec<String>,
annotations: Vec<Annotation>,
udas: HashMap<String, String>,
}
/// Parse an "integer", allowing for occasional integers with trailing decimal zeroes
fn parse_int<T>(value: &str) -> Result<T, <T as str::FromStr>::Err>
where
T: str::FromStr,
{
// some integers are rendered with following decimal zeroes
if let Some(i) = value.find('.') {
let mut nonzero = false;
for c in value[i + 1..].chars() {
if c != '0' {
nonzero = true;
break;
}
}
if !nonzero {
return value[..i].parse();
}
}
value.parse()
}
/// Parse a status into a Status enum value
fn parse_status(value: &str) -> Result<Status, String> {
match value {
"pending" => Ok(Status::Pending),
"completed" => Ok(Status::Completed),
"deleted" => Ok(Status::Deleted),
"recurring" => Ok(Status::Recurring),
"waiting" => Ok(Status::Waiting),
_ => Err(format!("invalid status {}", value)),
}
}
/// Parse "L", "M", "H" into the Priority enum
fn parse_priority(value: &str) -> Result<Priority, String> {
match value {
"L" => Ok(Priority::L),
"M" => Ok(Priority::M),
"H" => Ok(Priority::H),
_ => Err(format!("invalid priority {}", value)),
}
}
/// Parse a UNIX timestamp into a UTC DateTime
fn parse_timestamp(value: &str) -> Result<Timestamp, <i64 as str::FromStr>::Err> {
Ok(Utc.timestamp(parse_int::<i64>(value)?, 0))
}
/// Parse depends, as a list of ,-separated UUIDs
fn parse_depends(value: &str) -> Result<Vec<Uuid>, uuid::parser::ParseError> {
value.split(',').map(|s| Uuid::parse_str(s)).collect()
}
/// Parse tags, as a list of ,-separated strings
fn parse_tags(value: &str) -> Vec<String> {
value.split(',').map(|s| s.to_string()).collect()
}
impl TaskBuilder {
pub fn new() -> Self {
Default::default()
}
pub fn set(mut self, name: &str, value: String) -> Self {
const ANNOTATION_PREFIX: &str = "annotation_";
if name.starts_with(ANNOTATION_PREFIX) {
let entry = parse_timestamp(&name[ANNOTATION_PREFIX.len()..]).unwrap();
self.annotations.push(Annotation {
entry,
description: value.to_string(),
});
return self;
}
match name {
"status" => self.status = Some(parse_status(&value).unwrap()),
"uuid" => self.uuid = Some(Uuid::parse_str(&value).unwrap()),
"entry" => self.entry = Some(parse_timestamp(&value).unwrap()),
"description" => self.description = Some(value),
"start" => self.start = Some(parse_timestamp(&value).unwrap()),
"end" => self.end = Some(parse_timestamp(&value).unwrap()),
"due" => self.due = Some(parse_timestamp(&value).unwrap()),
"until" => self.until = Some(parse_timestamp(&value).unwrap()),
"wait" => self.wait = Some(parse_timestamp(&value).unwrap()),
"modified" => self.modified = Some(parse_timestamp(&value).unwrap()),
"scheduled" => self.scheduled = Some(parse_timestamp(&value).unwrap()),
"recur" => self.recur = Some(value),
"mask" => self.mask = Some(value),
"imask" => self.imask = Some(parse_int::<u64>(&value).unwrap()),
"parent" => self.uuid = Some(Uuid::parse_str(&value).unwrap()),
"project" => self.project = Some(value),
"priority" => self.priority = Some(parse_priority(&value).unwrap()),
"depends" => self.depends = parse_depends(&value).unwrap(),
"tags" => self.tags = parse_tags(&value),
_ => {
self.udas.insert(name.to_string(), value);
}
}
self
}
pub fn finish(self) -> Task {
Task {
status: self.status.unwrap(),
uuid: self.uuid.unwrap(),
description: self.description.unwrap(),
entry: self.entry.unwrap(),
start: self.start,
end: self.end,
due: self.due,
until: self.until,
wait: self.wait,
modified: self.modified.unwrap(),
scheduled: self.scheduled,
recur: self.recur,
mask: self.mask,
imask: self.imask,
parent: self.parent,
project: self.project,
priority: self.priority,
depends: self.depends,
tags: self.tags,
annotations: self.annotations,
udas: self.udas,
}
// TODO: check validity per https://taskwarrior.org/docs/design/task.html
}
}
#[cfg(test)]
mod test {
use super::{parse_int, parse_depends};
use uuid::Uuid;
#[test]
fn test_parse_int() {
assert_eq!(parse_int::<u8>("123").unwrap(), 123u8);
assert_eq!(parse_int::<u32>("123000000").unwrap(), 123000000u32);
assert_eq!(parse_int::<i32>("-123000000").unwrap(), -123000000i32);
}
#[test]
fn test_parse_int_decimals() {
assert_eq!(parse_int::<u8>("123.00").unwrap(), 123u8);
assert_eq!(parse_int::<u32>("123.0000").unwrap(), 123u32);
assert_eq!(parse_int::<i32>("-123.").unwrap(), -123i32);
}
#[test]
fn test_parse_depends() {
let u1 = "123e4567-e89b-12d3-a456-426655440000";
let u2 = "123e4567-e89b-12d3-a456-999999990000";
assert_eq!(
parse_depends(u1).unwrap(),
vec![Uuid::parse_str(u1).unwrap()]
);
assert_eq!(
parse_depends(&format!("{},{}", u1, u2)).unwrap(),
vec![Uuid::parse_str(u1).unwrap(), Uuid::parse_str(u2).unwrap()]
);
}
}

View File

@@ -1,9 +1,8 @@
use std::str;
use std::io::{Result, Error, ErrorKind};
use std::collections::HashMap;
use super::nibbler::Nibbler;
use super::super::task::Task;
use task::{TaskBuilder, Task};
/// Rust implementation of part of utf8_codepoint from Taskwarrior's src/utf8.cpp
///
@@ -88,7 +87,7 @@ fn decode(value: String) -> String {
/// While Taskwarrior supports additional formats, this is the only format supported by rask.
pub(super) fn parse_ff4(line: &str) -> Result<Task> {
let mut nib = Nibbler::new(line.as_bytes());
let mut data = HashMap::new();
let mut builder = TaskBuilder::new();
if !nib.skip(b'[') {
return Err(Error::new(ErrorKind::Other, "bad line"));
@@ -97,13 +96,14 @@ pub(super) fn parse_ff4(line: &str) -> Result<Task> {
let mut nib = Nibbler::new(line);
while !nib.depleted() {
if let Some(name) = nib.get_until(b':') {
let name = str::from_utf8(name).unwrap();
if !nib.skip(b':') {
return Err(Error::new(ErrorKind::Other, "bad line"));
}
if let Some(value) = nib.get_quoted(b'"') {
let value = json_decode(value);
let value = decode(value);
data.insert(String::from_utf8(name.to_vec()).unwrap(), value);
builder = builder.set(name, value);
} else {
return Err(Error::new(ErrorKind::Other, "bad line"));
}
@@ -121,12 +121,13 @@ pub(super) fn parse_ff4(line: &str) -> Result<Task> {
if !nib.depleted() {
return Err(Error::new(ErrorKind::Other, "bad line"));
}
Ok(Task::from_data(data))
Ok(builder.finish())
}
#[cfg(test)]
mod test {
use super::{decode, json_decode, hex_to_unicode, parse_ff4};
use task::Pending;
#[test]
fn test_hex_to_unicode_digits() {
@@ -217,7 +218,11 @@ mod test {
#[test]
fn test_parse_ff4() {
let task = parse_ff4("[description:\"desc\"]").unwrap();
assert_eq!(task.description(), "desc");
let s = "[description:\"desc\" entry:\"1437855511\" modified:\"1479480556\" \
priority:\"L\" project:\"lists\" status:\"pending\" tags:\"watch\" \
uuid:\"83ce989e-8634-4d62-841c-eb309383ff1f\"]";
let task = parse_ff4(s).unwrap();
assert_eq!(task.status, Pending);
assert_eq!(task.description, "desc");
}
}