diff options
author | Imran Iqbal <imran@imraniqbal.org> | 2022-12-27 16:38:20 -0500 |
---|---|---|
committer | Imran Iqbal <imran@imraniqbal.org> | 2022-12-29 13:28:50 -0500 |
commit | 14592e2cf4d5a5589971847e2d6ebad94e227504 (patch) | |
tree | b4fc67dfa3ec73950d4f7d0df63e5ad342b6098f | |
parent | 15e3895e6f7af0a9fdf3c6bb7e8c3131a3e81fa8 (diff) |
Support taskwarrior 2.6.0 serialization format
As of taskwarrior 2.6.0 the depends field changed from being a string of
comma seperated uuid's to being a proper json array of uuid strings.
This commit allows the selection of which serialization format to use
with the help of typestating. Task<TW26> will (de)serialize to the new format,
while Task<TW25> will retain the older behaviour. This will allow this
library to be used with any version of taskwarrior.
Signed-off-by: Imran Iqbal <imran@imraniqbal.org>
-rw-r--r-- | examples/create_task.rs | 3 | ||||
-rw-r--r-- | examples/import_task.rs | 3 | ||||
-rw-r--r-- | src/import.rs | 163 | ||||
-rw-r--r-- | src/lib.rs | 4 | ||||
-rw-r--r-- | src/task.rs | 526 |
5 files changed, 349 insertions, 350 deletions
diff --git a/examples/create_task.rs b/examples/create_task.rs index e95aedd..0567345 100644 --- a/examples/create_task.rs +++ b/examples/create_task.rs @@ -5,6 +5,7 @@ extern crate uuid; use task_hookrs::status::TaskStatus; use task_hookrs::task::Task; +use task_hookrs::task::TW26; use task_hookrs::uda::UDA; use chrono::NaiveDateTime; @@ -14,7 +15,7 @@ use uuid::Uuid; fn main() { let uuid = Uuid::nil(); let date = NaiveDateTime::parse_from_str("2016-12-31 12:13:14", "%Y-%m-%d %H:%M:%S").unwrap(); - let t = Task::new( + let t: Task<TW26> = Task::new( Some(12), TaskStatus::Pending, uuid, diff --git a/examples/import_task.rs b/examples/import_task.rs index c806bf6..11622d2 100644 --- a/examples/import_task.rs +++ b/examples/import_task.rs @@ -7,11 +7,12 @@ use std::io::stdin; use task_hookrs::import::import; use task_hookrs::status::TaskStatus; +use task_hookrs::task::Task; fn main() { let mut tasks = import(stdin()).unwrap(); assert_eq!(tasks.len(), 1); - let t = tasks.pop().unwrap(); + let t: Task = tasks.pop().unwrap(); assert_eq!(*t.status(), TaskStatus::Pending); assert_eq!(*t.description(), "Test task".to_owned()); assert_eq!(t.priority(), None); diff --git a/src/import.rs b/src/import.rs index a82d172..51e2a6b 100644 --- a/src/import.rs +++ b/src/import.rs @@ -12,21 +12,21 @@ use std::io::Read; use serde_json; use crate::error::Error; -use crate::task::Task; +use crate::task::{Task, TaskWarriorVersion}; /// Import taskwarrior-exported JSON. This expects an JSON Array of objects, as exported by /// taskwarrior. -pub fn import<R: Read>(r: R) -> Result<Vec<Task>, Error> { +pub fn import<T: TaskWarriorVersion, R: Read>(r: R) -> Result<Vec<Task<T>>, Error> { serde_json::from_reader(r).map_err(Error::from) } /// Import a single JSON-formatted Task -pub fn import_task(s: &str) -> Result<Task, Error> { +pub fn import_task<T: TaskWarriorVersion>(s: &str) -> Result<Task<T>, Error> { serde_json::from_str(s).map_err(Error::from) } /// Reads line by line and tries to parse a task-object per line. -pub fn import_tasks<BR: BufRead>(r: BR) -> Vec<Result<Task, Error>> { +pub fn import_tasks<T: TaskWarriorVersion, BR: BufRead>(r: BR) -> Vec<Result<Task<T>, Error>> { let mut vt = Vec::new(); for line in r.lines() { if let Err(err) = line { @@ -43,9 +43,14 @@ pub fn import_tasks<BR: BufRead>(r: BR) -> Vec<Result<Task, Error>> { vt } -#[test] -fn test_one() { - let s = r#" +#[cfg(test)] +mod test { + use crate::import::{import, import_task, import_tasks}; + use crate::task::{Task, TW25, TW26}; + + #[test] + fn test_one_tw25() { + let s = r#" [ { "id": 1, @@ -56,21 +61,48 @@ fn test_one() { "status": "waiting", "tags": ["some", "tags", "are", "here"], "uuid": "8ca953d5-18b4-4eb9-bd56-18f2e5b752f0", + "depends": "8ca953d5-18b5-4eb9-bd56-18f2e5b752f0", "wait": "20160508T164007Z", "urgency": 0.583562 } ] "#; - let imported = import(s.as_bytes()); - assert!(imported.is_ok()); - let imported = imported.unwrap(); - assert!(imported.len() == 1); -} + let imported = import::<TW25, _>(s.as_bytes()); + assert!(imported.is_ok()); + let imported = imported.unwrap(); + assert!(imported.len() == 1); + } + + #[test] + fn test_one_tw26() { + let s = r#" +[ + { + "id": 1, + "description": "some description", + "entry": "20150619T165438Z", + "modified": "20160327T164007Z", + "project": "someproject", + "status": "waiting", + "tags": ["some", "tags", "are", "here"], + "uuid": "8ca953d5-18b4-4eb9-bd56-18f2e5b752f0", + "depends": ["8ca953d5-18b5-4eb9-bd56-18f2e5b752f0"], + "wait": "20160508T164007Z", + "urgency": 0.583562 + } +] +"#; -#[test] -fn test_two() { - let s = r#" + let imported = import::<TW26, _>(s.as_bytes()); + assert!(imported.is_ok()); + let imported = imported.unwrap(); + assert!(imported.len() == 1); + } + + #[test] + fn test_two_tw25() { + let s = r#" [ { "id" : 1, @@ -119,21 +151,21 @@ fn test_two() { "#; - assert!(import(s.as_bytes()).unwrap().len() == 3); -} - -#[test] -fn test_one_single() { - use crate::date::Date; - use crate::date::TASKWARRIOR_DATETIME_TEMPLATE; - use crate::status::TaskStatus; - use chrono::NaiveDateTime; - use uuid::Uuid; - fn mkdate(s: &str) -> Date { - let n = NaiveDateTime::parse_from_str(s, TASKWARRIOR_DATETIME_TEMPLATE); - Date::from(n.unwrap()) + assert!(import::<TW25, _>(s.as_bytes()).unwrap().len() == 3); } - let s = r#" + + #[test] + fn test_one_single_tw25() { + use crate::date::Date; + use crate::date::TASKWARRIOR_DATETIME_TEMPLATE; + use crate::status::TaskStatus; + use chrono::NaiveDateTime; + use uuid::Uuid; + fn mkdate(s: &str) -> Date { + let n = NaiveDateTime::parse_from_str(s, TASKWARRIOR_DATETIME_TEMPLATE); + Date::from(n.unwrap()) + } + let s = r#" { "id": 1, "description": "some description", @@ -147,45 +179,46 @@ fn test_one_single() { "urgency": 0.583562 } "#; - let imported = import_task(s); - assert!(imported.is_ok()); - - // Check for every information - let task = imported.unwrap(); - assert_eq!(*task.status(), TaskStatus::Waiting); - assert_eq!(task.description(), "some description"); - assert_eq!(*task.entry(), mkdate("20150619T165438Z")); - assert_eq!( - *task.uuid(), - Uuid::parse_str("8ca953d5-18b4-4eb9-bd56-18f2e5b752f0").unwrap() - ); - assert_eq!(task.modified(), Some(&mkdate("20160327T164007Z"))); - assert_eq!(task.project(), Some(&String::from("someproject"))); - if let Some(tags) = task.tags() { - for tag in tags { - let any_tag = ["some", "tags", "are", "here"].iter().any(|t| tag == *t); - assert!(any_tag, "Tag {} missing", tag); + let imported = import_task(s); + assert!(imported.is_ok()); + + // Check for every information + let task: Task<TW25> = imported.unwrap(); + assert_eq!(*task.status(), TaskStatus::Waiting); + assert_eq!(task.description(), "some description"); + assert_eq!(*task.entry(), mkdate("20150619T165438Z")); + assert_eq!( + *task.uuid(), + Uuid::parse_str("8ca953d5-18b4-4eb9-bd56-18f2e5b752f0").unwrap() + ); + assert_eq!(task.modified(), Some(&mkdate("20160327T164007Z"))); + assert_eq!(task.project(), Some(&String::from("someproject"))); + if let Some(tags) = task.tags() { + for tag in tags { + let any_tag = ["some", "tags", "are", "here"].iter().any(|t| tag == *t); + assert!(any_tag, "Tag {} missing", tag); + } + } else { + panic!("Tags completely missing"); } - } else { - panic!("Tags completely missing"); - } - assert_eq!(task.wait(), Some(&mkdate("20160508T164007Z"))); -} + assert_eq!(task.wait(), Some(&mkdate("20160508T164007Z"))); + } -#[test] -fn test_two_single() { - use crate::status::TaskStatus; - use std::io::BufReader; - let s = r#" + #[test] + fn test_two_single_tw25() { + use crate::status::TaskStatus; + use std::io::BufReader; + let s = r#" {"id":1,"description":"some description","entry":"20150619T165438Z","modified":"20160327T164007Z","project":"someproject","status":"waiting","tags":["some","tags","are","here"],"uuid":"8ca953d5-18b4-4eb9-bd56-18f2e5b752f0","wait":"20160508T164007Z","urgency":0.583562} {"id":1,"description":"some description","entry":"20150619T165438Z","modified":"20160327T164007Z","project":"someproject","status":"waiting","tags":["some","tags","are","here"],"uuid":"8ca953d5-18b4-4eb9-bd56-18f2e5b752f0","wait":"20160508T164007Z","urgency":0.583562}"#; - let imported = import_tasks(BufReader::new(s.as_bytes())); - assert_eq!(imported.len(), 2); - assert!(imported[0].is_ok()); - assert!(imported[1].is_ok()); - let import0 = imported[0].as_ref().unwrap(); - let import1 = imported[1].as_ref().unwrap(); - assert_eq!(*import0.status(), TaskStatus::Waiting); - assert_eq!(*import1.status(), TaskStatus::Waiting); + let imported = import_tasks(BufReader::new(s.as_bytes())); + assert_eq!(imported.len(), 2); + assert!(imported[0].is_ok()); + assert!(imported[1].is_ok()); + let import0: &Task<TW25> = imported[0].as_ref().unwrap(); + let import1 = imported[1].as_ref().unwrap(); + assert_eq!(*import0.status(), TaskStatus::Waiting); + assert_eq!(*import1.status(), TaskStatus::Waiting); + } } @@ -12,10 +12,10 @@ //! ``` //! use std::io::stdin; //! -//! use task_hookrs::task::Task; +//! use task_hookrs::task::{Task, TW26}; //! use task_hookrs::import::import; //! -//! if let Ok(tasks) = import(stdin()) { +//! if let Ok(tasks) = import::<TW26, _>(stdin()) { //! for task in tasks { //! println!("Task: {}, entered {:?} is {} -> {}", //! task.uuid(), diff --git a/src/task.rs b/src/task.rs index 8cf65d7..46690ce 100644 --- a/src/task.rs +++ b/src/task.rs @@ -6,13 +6,11 @@ //! Module containing `Task` type as well as trait implementations -use std::fmt; +use std::marker::PhantomData; use std::result::Result as RResult; use chrono::Utc; -use serde::de::{Error, MapAccess, Visitor}; -use serde::ser::SerializeMap; -use serde::{Deserialize, Deserializer}; +use serde::{de, Deserialize, Deserializer}; use serde::{Serialize, Serializer}; use uuid::Uuid; @@ -22,9 +20,31 @@ use crate::priority::TaskPriority; use crate::project::Project; use crate::status::TaskStatus; use crate::tag::Tag; -use crate::uda::{UDAName, UDAValue, UDA}; +use crate::uda::UDA; use crate::urgency::Urgency; +/// Unit struct used to represent taskwarrior format 2.6.0 and newer. +/// See [Task] for more information. +#[derive(Debug, Clone)] +pub struct TW26; + +/// Unit struct used to represent taskwarrior format 2.5.3 and older. +/// See [Task] for more information. +#[derive(Debug, Clone)] +pub struct TW25; + +// Prevents folks outside this crate from implementing their own versions +mod private { + pub trait Sealed {} + impl Sealed for super::TW26 {} + impl Sealed for super::TW25 {} +} + +/// Trait used to represent taskwarrior version types +pub trait TaskWarriorVersion: private::Sealed {} +impl TaskWarriorVersion for TW26 {} +impl TaskWarriorVersion for TW25 {} + /// Task type /// /// A task must have four things: @@ -41,11 +61,17 @@ use crate::urgency::Urgency; /// /// It is deserializeable and serializeable via serde_json, so importing and exporting taskwarrior /// tasks is simply serializing and deserializing objects of this type. -#[derive(Debug, Clone, PartialEq, derive_builder::Builder)] +/// +/// As of taskwarrior version 2.6.0 and newer, the representation of `depends` has changed from +/// being a comma seperated string of uuid's to being a proper json array. You can select which +/// behaviour you want at compiletime by providing either [TW26] (the default) or [TW25] to `Task` as its +/// type parameter. +#[derive(Debug, Clone, PartialEq, derive_builder::Builder, Serialize, Deserialize)] #[builder(setter(into))] -pub struct Task { +pub struct Task<Version: TaskWarriorVersion + 'static = TW26> { /// The temporary assigned task id #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] id: Option<u64>, /// The status of the task @@ -62,66 +88,93 @@ pub struct Task { description: String, /// A list of annotations with timestamps #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] annotations: Option<Vec<Annotation>>, /// The uuids of other tasks which have to be completed before this one becomes unblocked. #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(serialize_with = "serialize_depends::<_, Version>")] + #[serde(deserialize_with = "deserialize_depends::<_, Version>", default)] depends: Option<Vec<Uuid>>, /// The due date of the task #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] due: Option<Date>, /// When the task was last deleted or completed #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] end: Option<Date>, /// The imask is used internally for recurrence #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] imask: Option<f64>, /// The mask is used internally for recurrence #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] mask: Option<String>, /// When the task was last modified #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] modified: Option<Date>, /// A task can have a parent task #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] parent: Option<Uuid>, /// The priority of the task #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] priority: Option<TaskPriority>, /// A task can be part of a project. Typically of the form "project.subproject.subsubproject" #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] project: Option<Project>, /// The timespan after which this task should recur #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] recur: Option<String>, /// When the task becomes ready #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] scheduled: Option<Date>, /// When the task becomes active #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] start: Option<Date>, /// The tags associated with the task #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] tags: Option<Vec<Tag>>, /// When the recurrence stops #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] until: Option<Date>, /// This hides the task until the wait date #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] wait: Option<Date>, /// This contains the urgency of the task #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] urgency: Option<Urgency>, /// A map of user defined attributes #[builder(default)] + #[serde(default)] + #[serde(skip_serializing_if = "UDA::is_empty")] + #[serde(flatten)] uda: UDA, + + #[doc(hidden)] + #[builder(setter(skip))] + #[serde(skip)] + _version: PhantomData<Version>, } /* * TODO: We do not fail if the JSON parsing fails. This panics. We rely on taskwarrior to be nice * to us. I guess this should be fixed. */ -impl Task { +impl<Version: TaskWarriorVersion> Task<Version> { /// Create a new Task instance #[allow(clippy::too_many_arguments)] pub fn new( @@ -150,7 +203,7 @@ impl Task { wait: Option<Date>, urgency: Option<Urgency>, uda: UDA, - ) -> Task { + ) -> Task<Version> { Task { id, status, @@ -176,6 +229,7 @@ impl Task { wait, urgency, uda, + _version: PhantomData, } } @@ -546,273 +600,36 @@ impl Task { } } -impl Serialize for Task { - fn serialize<S>(&self, serializer: S) -> RResult<S::Ok, S::Error> - where - S: Serializer, - { - let mut state = serializer.serialize_map(None)?; - state.serialize_entry("status", &self.status)?; - state.serialize_entry("uuid", &self.uuid)?; - state.serialize_entry("entry", &self.entry)?; - state.serialize_entry("description", &self.description)?; - - self.annotations - .as_ref() - .map(|v| state.serialize_entry("annotations", v)); - self.tags.as_ref().map(|v| state.serialize_entry("tags", v)); - self.id.as_ref().map(|v| state.serialize_entry("id", v)); - self.recur - .as_ref() - .map(|ref v| state.serialize_entry("recur", v)); - self.depends.as_ref().map(|v| { - let v: Vec<String> = v.iter().map(Uuid::to_string).collect(); - state.serialize_entry("depends", &v.join(",")) - }); - self.due - .as_ref() - .map(|ref v| state.serialize_entry("due", v)); - self.end - .as_ref() - .map(|ref v| state.serialize_entry("end", v)); - self.imask - .as_ref() - .map(|ref v| state.serialize_entry("imask", v)); - self.mask - .as_ref() - .map(|ref v| state.serialize_entry("mask", v)); - self.modified - .as_ref() - .map(|ref v| state.serialize_entry("modified", v)); - self.parent - .as_ref() - .map(|ref v| state.serialize_entry("parent", v)); - self.priority - .as_ref() - .map(|ref v| state.serialize_entry("priority", v)); - self.project - .as_ref() - .map(|ref v| state.serialize_entry("project", v)); - self.scheduled - .as_ref() - .map(|ref v| state.serialize_entry("scheduled", v)); - self.start - .as_ref() - .map(|ref v| state.serialize_entry("start", v)); - self.until - .as_ref() - .map(|ref v| state.serialize_entry("until", v)); - self.wait - .as_ref() - .map(|ref v| state.serialize_entry("wait", v)); - self.urgency - .as_ref() - .map(|ref v| state.serialize_entry("urgency", v)); - - for (key, value) in self.uda().iter() { - state.serialize_entry(key, value)?; - } - - state.end() +fn serialize_depends<S, T: 'static>( + field: &Option<Vec<Uuid>>, + serializer: S, +) -> RResult<S::Ok, S::Error> +where + S: Serializer, +{ + if std::any::TypeId::of::<T>() == std::any::TypeId::of::<TW25>() { + let value = field.as_ref().unwrap(); + let v: Vec<String> = value.iter().map(Uuid::to_string).collect(); + serializer.serialize_str(&v.join(",")) + } else { + field.serialize(serializer) } } -impl<'de> Deserialize<'de> for Task { - fn deserialize<D>(deserializer: D) -> RResult<Task, D::Error> - where - D: Deserializer<'de>, - { - static FIELDS: &[&str] = &[ - "id", - "status", - "uuid", - "entry", - "description", - "annotations", - "depends", - "due", - "end", - "imask", - "mask", - "modified", - "parent", - "priority", - "project", - "recur", - "scheduled", - "start", - "tags", - "until", - "wait", - "urgency", - "uda", - ]; - deserializer.deserialize_struct("Task", FIELDS, TaskDeserializeVisitor) - } -} - -/// Helper type for task deserialization -struct TaskDeserializeVisitor; - -impl<'de> Visitor<'de> for TaskDeserializeVisitor { - type Value = Task; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - write!(formatter, "A dictionary containing task properties") - } - - fn visit_map<V>(self, mut visitor: V) -> RResult<Task, V::Error> - where - V: MapAccess<'de>, - { - let mut id = None; - - let mut status = None; - let mut uuid = None; - let mut entry = None; - let mut description = None; - - let mut annotations = None; - let mut depends = None; - let mut due = None; - let mut end = None; - let mut imask = None; - let mut mask = None; - let mut modified = None; - let mut parent = None; - let mut priority = None; - let mut project = None; - let mut recur = None; - let mut scheduled = None; - let mut start = None; - let mut tags = None; - let mut until = None; - let mut wait = None; - let mut urgency = None; - let mut uda = UDA::default(); - - loop { - let key: Option<String> = visitor.next_key()?; - if key.is_none() { - break; - } - let key = key.unwrap(); - - match &key[..] { - "id" => { - id = Some(visitor.next_value()?); - } - - "status" => { - status = Some(visitor.next_value()?); - } - "uuid" => { - uuid = Some(visitor.next_value()?); - } - "entry" => { - entry = Some(visitor.next_value()?); - } - "description" => { - description = Some(visitor.next_value()?); - } - - "annotations" => { - annotations = Some(visitor.next_value()?); - } - "depends" => { - let raw: String = visitor.next_value()?; - let mut uuids = vec![]; - for uuid in raw.split(',') { - uuids.push(Uuid::parse_str(uuid).map_err(V::Error::custom)?); - } - depends = Some(uuids); - } - "due" => { - due = Some(visitor.next_value()?); - } - "end" => { - end = Some(visitor.next_value()?); - } - "imask" => { - imask = Some(visitor.next_value()?); - } - "mask" => { - mask = Some(visitor.next_value()?); - } - "modified" => { - modified = Some(visitor.next_value()?); - } - "parent" => { - parent = Some(visitor.next_value()?); - } - "priority" => { - priority = Some(visitor.next_value()?); - } - "project" => { - project = Some(visitor.next_value()?); - } - "recur" => { - recur = Some(visitor.next_value()?); - } - "scheduled" => { - scheduled = Some(visitor.next_value()?); - } - "start" => { - start = Some(visitor.next_value()?); - } - "tags" => { - tags = Some(visitor.next_value()?); - } - "until" => { - until = Some(visitor.next_value()?); - } - "wait" => { - wait = Some(visitor.next_value()?); - } - "urgency" => { - urgency = Some(visitor.next_value()?); - } - - field => { - log::debug!("Inserting '{}' as UDA", field); - let uda_value: UDAValue = visitor.next_value()?; - uda.insert(UDAName::from(field), uda_value); - } - } +fn deserialize_depends<'de, D, T: 'static>(deserializer: D) -> RResult<Option<Vec<Uuid>>, D::Error> +where + D: Deserializer<'de>, +{ + if std::any::TypeId::of::<T>() == std::any::TypeId::of::<TW25>() { + let raw: String = String::deserialize(deserializer)?; + let mut uuids = vec![]; + for uuid in raw.split(',') { + uuids.push(Uuid::parse_str(uuid).map_err(de::Error::custom)?); } - - let status = status.ok_or_else(|| V::Error::missing_field("status"))?; - let uuid = uuid.ok_or_else(|| V::Error::missing_field("uuid"))?; - let entry = entry.ok_or_else(|| V::Error::missing_field("entry"))?; - let description = description.ok_or_else(|| V::Error::missing_field("description"))?; - - let task = Task::new( - id, - status, - uuid, - entry, - description, - annotations, - depends, - due, - end, - imask, - mask, - modified, - parent, - priority, - project, - recur, - scheduled, - start, - tags, - until, - wait, - urgency, - uda, - ); - - Ok(task) + Ok(Some(uuids)) + } else { + let value: Option<Vec<Uuid>> = Option::deserialize(deserializer)?; + Ok(value) } } @@ -822,12 +639,12 @@ mod test { use crate::date::Date; use crate::date::TASKWARRIOR_DATETIME_TEMPLATE; use crate::status::TaskStatus; - use crate::task::Task; + use crate::task::{Task, TW25, TW26}; use crate::uda::UDAValue; use chrono::NaiveDateTime; use serde_json; - use uuid::Uuid; + use uuid::{uuid, Uuid}; fn mklogger() { let _ = env_logger::init(); @@ -880,7 +697,80 @@ mod test { } #[test] - fn test_deser_more() { + fn test_deser_more_tw26() { + let s = r#"{ +"id": 1, +"description": "some description", +"entry": "20150619T165438Z", +"modified": "20160327T164007Z", +"project": "someproject", +"status": "waiting", +"tags": ["some", "tags", "are", "here"], +"uuid": "8ca953d5-18b4-4eb9-bd56-18f2e5b752f0", +"depends": ["8ca953d5-18b4-4eb9-bd56-18f2e5b752f0","5a04bb1e-3f4b-49fb-b9ba-44407ca223b5"], +"wait": "20160508T164007Z", +"urgency": 0.583562 +}"#; + let task = serde_json::from_str(s); + assert!(task.is_ok()); + let task: Task = task.unwrap(); + + assert_eq!(*task.status(), TaskStatus::Waiting); + assert_eq!(task.description(), "some description"); + assert_eq!(*task.entry(), mkdate("20150619T165438Z")); + assert_eq!( + *task.uuid(), + Uuid::parse_str("8ca953d5-18b4-4eb9-bd56-18f2e5b752f0").unwrap() + ); + assert_eq!(task.urgency(), Some(&0.583562)); + assert_eq!(task.modified(), Some(&mkdate("20160327T164007Z"))); + assert_eq!(task.project(), Some(&String::from("someproject"))); + + if let Some(tags) = task.tags() { + for tag in tags { + let any_tag = ["some", "tags", "are", "here"].iter().any(|t| tag == *t); + assert!(any_tag, "Tag {} missing", tag); + } + } else { + panic!("Tags completely missing"); + } + + assert_eq!(task.wait(), Some(&mkdate("20160508T164007Z"))); + + if let Some(depends) = task.depends() { + assert_eq!(depends.len(), 2); + assert!(depends.contains(&uuid!("8ca953d5-18b4-4eb9-bd56-18f2e5b752f0"))); + assert!(depends.contains(&uuid!("5a04bb1e-3f4b-49fb-b9ba-44407ca223b5"))); + } else { + panic!("Depends completely missing"); + } + + assert_eq!(task.wait(), Some(&mkdate("20160508T164007Z"))); + + let back = serde_json::to_string(&task).unwrap(); + + assert!(back.contains("description")); + assert!(back.contains("some description")); + assert!(back.contains("entry")); + assert!(back.contains("20150619T165438Z")); + assert!(back.contains("project")); + assert!(back.contains("someproject")); + assert!(back.contains("status")); + assert!(back.contains("waiting")); + assert!(back.contains("tags")); + assert!(back.contains("some")); + assert!(back.contains("tags")); + assert!(back.contains("are")); + assert!(back.contains("here")); + assert!(back.contains("uuid")); + assert!(back.contains("8ca953d5-18b4-4eb9-bd56-18f2e5b752f0")); + assert!(back.contains( + r#"["8ca953d5-18b4-4eb9-bd56-18f2e5b752f0","5a04bb1e-3f4b-49fb-b9ba-44407ca223b5"]"#, + )); + } + + #[test] + fn test_deser_more_tw25() { mklogger(); let s = r#"{ "id": 1, @@ -901,7 +791,7 @@ mod test { let task = serde_json::from_str(s); println!("{:?}", task); assert!(task.is_ok()); - let task: Task = task.unwrap(); + let task: Task<TW25> = task.unwrap(); assert_eq!(*task.status(), TaskStatus::Waiting); assert_eq!(task.description(), "some description"); @@ -923,6 +813,14 @@ mod test { panic!("Tags completely missing"); } + if let Some(depends) = task.depends() { + assert_eq!(depends.len(), 2); + assert!(depends.contains(&uuid!("8ca953d5-18b4-4eb9-bd56-18f2e5b752f0"))); + assert!(depends.contains(&uuid!("5a04bb1e-3f4b-49fb-b9ba-44407ca223b5"))); + } else { + panic!("Depends completely missing"); + } + assert_eq!(task.wait(), Some(&mkdate("20160508T164007Z"))); let back = serde_json::to_string(&task).unwrap(); @@ -1018,7 +916,7 @@ mod test { let task = serde_json::from_str(s); println!("{:?}", task); assert!(task.is_ok()); - let task: Task = task.unwrap(); + let task: Task<TW25> = task.unwrap(); let str_uda = task.uda().get(&"test_str_uda".to_owned()); assert!(str_uda.is_some()); @@ -1101,7 +999,7 @@ mod test { fn test_builder_simple() { use crate::task::TaskBuilder; - let t = TaskBuilder::default() + let t = TaskBuilder::<TW25>::default() .description("test") .entry(mkdate("20150619T165438Z")) .build(); @@ -1116,6 +1014,7 @@ mod test { #[test] fn test_builder_extensive() { use crate::task::TaskBuilder; + use crate::task::TW25; use crate::uda::{UDAValue, UDA}; let mut uda = UDA::new(); uda.insert( @@ -1124,7 +1023,7 @@ mod test { ); uda.insert("test_int_uda".into(), UDAValue::U64(1234)); uda.insert("test_float_uda".into(), UDAValue::F64(-17.1234)); - let t = TaskBuilder::default() + let t = TaskBuilder::<TW25>::default() .description("test") .entry(mkdate("20150619T165438Z")) .id(192) @@ -1156,7 +1055,7 @@ mod test { #[test] fn test_builder_defaults() { use crate::task::TaskBuilder; - assert!(TaskBuilder::default() + assert!(TaskBuilder::<TW25>::default() .description("Nice Task") .build() .is_ok()); @@ -1165,6 +1064,71 @@ mod test { #[test] fn test_builder_fail() { use crate::task::TaskBuilder; - assert!(TaskBuilder::default().build().is_err()); + assert!(TaskBuilder::<TW25>::default().build().is_err()); + } + + const FIELD_NAMES_TO_NOT_SERIALIZE: [&str; 20] = [ + r#""id":"#, + r#"""annotations:""#, + r#""depends:""#, + r#""due:""#, + r#""end:""#, + r#""imask:""#, + r#""mask:""#, + r#""modified:""#, + r#""parent:""#, + r#""priority:""#, + r#""project:""#, + r#""recur:""#, + r#""scheduled:""#, + r#""start:""#, + r#""tags:""#, + r#""until:""#, + r#""wait:""#, + r#""urgency:""#, + r#""uda:""#, + r#""_version:""#, + ]; + + #[test] + fn test_null_fields_not_serialized_tw25() { + use crate::task::TaskBuilder; + + let task = TaskBuilder::<TW25>::default() + .description("Test Task") + .build() + .expect("Task to be built"); + + let task_as_str = serde_json::to_string_pretty(&task).expect("Task serialized as string"); + + for field_name in FIELD_NAMES_TO_NOT_SERIALIZE { + assert!( + !task_as_str.contains(field_name), + "'{}' should not have been in {}", + field_name, + task_as_str + ); + } + } + + #[test] + fn test_null_fields_not_serialized_tw26() { + use crate::task::TaskBuilder; + + let task = TaskBuilder::<TW26>::default() + .description("Test Task") + .build() + .expect("Task to be built"); + + let task_as_str = serde_json::to_string_pretty(&task).expect("Task serialized as string"); + + for field_name in FIELD_NAMES_TO_NOT_SERIALIZE { + assert!( + !task_as_str.contains(field_name), + "'{}' should not have been in {}", + field_name, + task_as_str + ); + } } } |