task_hookrs/
task.rs

1//
2// This Source Code Form is subject to the terms of the Mozilla Public
3// License, v. 2.0. If a copy of the MPL was not distributed with this
4// file, You can obtain one at http://mozilla.org/MPL/2.0/.
5//
6
7//! Module containing `Task` type as well as trait implementations
8
9use std::marker::PhantomData;
10use std::result::Result as RResult;
11
12use chrono::Utc;
13use serde::{de, Deserialize, Deserializer};
14use serde::{Serialize, Serializer};
15use uuid::Uuid;
16
17use crate::annotation::Annotation;
18use crate::date::Date;
19use crate::priority::TaskPriority;
20use crate::project::Project;
21use crate::status::TaskStatus;
22use crate::tag::Tag;
23use crate::uda::UDA;
24use crate::urgency::Urgency;
25
26/// Unit struct used to represent taskwarrior format 2.6.0 and newer.
27/// See [Task] for more information.
28#[derive(Debug, Clone)]
29pub struct TW26;
30
31/// Unit struct used to represent taskwarrior format 2.5.3 and older.
32/// See [Task] for more information.
33#[derive(Debug, Clone)]
34pub struct TW25;
35
36// Prevents folks outside this crate from implementing their own versions
37mod private {
38    pub trait Sealed {}
39    impl Sealed for super::TW26 {}
40    impl Sealed for super::TW25 {}
41}
42
43/// Trait used to represent taskwarrior version types
44pub trait TaskWarriorVersion: private::Sealed {}
45impl TaskWarriorVersion for TW26 {}
46impl TaskWarriorVersion for TW25 {}
47
48/// Task type
49///
50/// A task must have four things:
51///
52/// - A Status
53/// - An UUID
54/// - An Entry-Date
55/// - A Description
56///
57/// all other Data is optional by taskwarrior. This type is a simple rust representation of the
58/// JSON exported by taskwarrior.
59///
60/// For further explanations of the fields please consult the documentation on https://taskwarrior.org/
61///
62/// It is deserializeable and serializeable via serde_json, so importing and exporting taskwarrior
63/// tasks is simply serializing and deserializing objects of this type.
64///
65/// As of taskwarrior version 2.6.0 and newer, the representation of `depends` has changed from
66/// being a comma seperated string of uuid's to being a proper json array. You can select which
67/// behaviour you want at compiletime by providing either [TW26] (the default) or [TW25] to `Task` as its
68/// type parameter.
69#[derive(Debug, Clone, PartialEq, derive_builder::Builder, Serialize, Deserialize)]
70#[builder(setter(into))]
71pub struct Task<Version: TaskWarriorVersion + 'static = TW26> {
72    /// The temporary assigned task id
73    #[builder(default)]
74    #[serde(skip_serializing_if = "Option::is_none")]
75    id: Option<u64>,
76
77    /// The status of the task
78    #[builder(default = "TaskStatus::Pending")]
79    status: TaskStatus,
80    /// The uuid which identifies the task and is important for syncing
81    #[builder(default = "Uuid::new_v4()")]
82    uuid: Uuid,
83    /// The entry date, when this task was created
84    #[builder(default = "Date::from(Utc::now().naive_utc())")]
85    entry: Date,
86    /// The description of the task (i.e. its main content)
87    /// This field is the only mandatory field, when using the TaskBuilder.
88    description: String,
89    /// A list of annotations with timestamps
90    #[builder(default)]
91    #[serde(skip_serializing_if = "Option::is_none")]
92    annotations: Option<Vec<Annotation>>,
93    /// The uuids of other tasks which have to be completed before this one becomes unblocked.
94    #[builder(default)]
95    #[serde(skip_serializing_if = "Option::is_none")]
96    #[serde(serialize_with = "serialize_depends::<_, Version>")]
97    #[serde(deserialize_with = "deserialize_depends::<_, Version>", default)]
98    depends: Option<Vec<Uuid>>,
99    /// The due date of the task
100    #[builder(default)]
101    #[serde(skip_serializing_if = "Option::is_none")]
102    due: Option<Date>,
103    /// When the task was last deleted or completed
104    #[builder(default)]
105    #[serde(skip_serializing_if = "Option::is_none")]
106    end: Option<Date>,
107    /// The imask is used internally for recurrence
108    #[builder(default)]
109    #[serde(skip_serializing_if = "Option::is_none")]
110    imask: Option<f64>,
111    /// The mask is used internally for recurrence
112    #[builder(default)]
113    #[serde(skip_serializing_if = "Option::is_none")]
114    mask: Option<String>,
115    /// When the task was last modified
116    #[builder(default)]
117    #[serde(skip_serializing_if = "Option::is_none")]
118    modified: Option<Date>,
119    /// A task can have a parent task
120    #[builder(default)]
121    #[serde(skip_serializing_if = "Option::is_none")]
122    parent: Option<Uuid>,
123    /// The priority of the task
124    #[builder(default)]
125    #[serde(skip_serializing_if = "Option::is_none")]
126    priority: Option<TaskPriority>,
127    /// A task can be part of a project. Typically of the form "project.subproject.subsubproject"
128    #[builder(default)]
129    #[serde(skip_serializing_if = "Option::is_none")]
130    project: Option<Project>,
131    /// The timespan after which this task should recur
132    #[builder(default)]
133    #[serde(skip_serializing_if = "Option::is_none")]
134    recur: Option<String>,
135    /// When the task becomes ready
136    #[builder(default)]
137    #[serde(skip_serializing_if = "Option::is_none")]
138    scheduled: Option<Date>,
139    /// When the task becomes active
140    #[builder(default)]
141    #[serde(skip_serializing_if = "Option::is_none")]
142    start: Option<Date>,
143    /// The tags associated with the task
144    #[builder(default)]
145    #[serde(skip_serializing_if = "Option::is_none")]
146    tags: Option<Vec<Tag>>,
147    /// When the recurrence stops
148    #[builder(default)]
149    #[serde(skip_serializing_if = "Option::is_none")]
150    until: Option<Date>,
151    /// This hides the task until the wait date
152    #[builder(default)]
153    #[serde(skip_serializing_if = "Option::is_none")]
154    wait: Option<Date>,
155    /// This contains the urgency of the task
156    #[builder(default)]
157    #[serde(skip_serializing_if = "Option::is_none")]
158    urgency: Option<Urgency>,
159
160    /// A map of user defined attributes
161    #[builder(default)]
162    #[serde(default)]
163    #[serde(skip_serializing_if = "UDA::is_empty")]
164    #[serde(flatten)]
165    uda: UDA,
166
167    #[doc(hidden)]
168    #[builder(setter(skip))]
169    #[serde(skip)]
170    _version: PhantomData<Version>,
171}
172
173/*
174 * TODO: We do not fail if the JSON parsing fails. This panics. We rely on taskwarrior to be nice
175 * to us. I guess this should be fixed.
176 */
177impl<Version: TaskWarriorVersion> Task<Version> {
178    /// Create a new Task instance
179    #[allow(clippy::too_many_arguments)]
180    pub fn new(
181        id: Option<u64>,
182
183        status: TaskStatus,
184        uuid: Uuid,
185        entry: Date,
186        description: String,
187
188        annotations: Option<Vec<Annotation>>,
189        depends: Option<Vec<Uuid>>,
190        due: Option<Date>,
191        end: Option<Date>,
192        imask: Option<f64>,
193        mask: Option<String>,
194        modified: Option<Date>,
195        parent: Option<Uuid>,
196        priority: Option<TaskPriority>,
197        project: Option<Project>,
198        recur: Option<String>,
199        scheduled: Option<Date>,
200        start: Option<Date>,
201        tags: Option<Vec<Tag>>,
202        until: Option<Date>,
203        wait: Option<Date>,
204        urgency: Option<Urgency>,
205        uda: UDA,
206    ) -> Task<Version> {
207        Task {
208            id,
209            status,
210            uuid,
211            entry,
212            description,
213
214            annotations,
215            depends,
216            due,
217            end,
218            imask,
219            mask,
220            modified,
221            parent,
222            priority,
223            project,
224            recur,
225            scheduled,
226            start,
227            tags,
228            until,
229            wait,
230            urgency,
231            uda,
232            _version: PhantomData,
233        }
234    }
235
236    /// Get the id of the task
237    pub fn id(&self) -> Option<u64> {
238        self.id
239    }
240
241    /// Get the status of the task
242    pub fn status(&self) -> &TaskStatus {
243        &self.status
244    }
245
246    /// Get the status of the task mutable
247    pub fn status_mut(&mut self) -> &mut TaskStatus {
248        &mut self.status
249    }
250
251    /// Get the uuid of the task
252    pub fn uuid(&self) -> &Uuid {
253        &self.uuid
254    }
255
256    /// Get the uuid of the task mutable
257    pub fn uuid_mut(&mut self) -> &mut Uuid {
258        &mut self.uuid
259    }
260
261    /// Get the entry date of the task
262    pub fn entry(&self) -> &Date {
263        &self.entry
264    }
265
266    /// Get the entry date of the task mutable
267    pub fn entry_mut(&mut self) -> &mut Date {
268        &mut self.entry
269    }
270
271    /// Get the description of the task
272    pub fn description(&self) -> &String {
273        &self.description
274    }
275
276    /// Get the description of the task mutable
277    pub fn description_mut(&mut self) -> &mut String {
278        &mut self.description
279    }
280
281    /// Get the annotations of the task
282    pub fn annotations(&self) -> Option<&Vec<Annotation>> {
283        self.annotations.as_ref()
284    }
285
286    /// Get the annotations of the task mutable
287    pub fn annotations_mut(&mut self) -> Option<&mut Vec<Annotation>> {
288        self.annotations.as_mut()
289    }
290
291    /// Set annotations
292    pub fn set_annotations<T, A>(&mut self, new: Option<T>)
293    where
294        T: IntoIterator,
295        T::Item: Into<Annotation>,
296    {
297        self.annotations = new.map(|x| x.into_iter().map(Into::into).collect());
298    }
299
300    /// Get the dependencies of the task
301    pub fn depends(&self) -> Option<&Vec<Uuid>> {
302        self.depends.as_ref()
303    }
304
305    /// Get the dependencies of the task mutable
306    pub fn depends_mut(&mut self) -> Option<&mut Vec<Uuid>> {
307        self.depends.as_mut()
308    }
309
310    /// Set depends
311    pub fn set_depends<T, U>(&mut self, new: Option<T>)
312    where
313        T: IntoIterator,
314        T::Item: Into<Uuid>,
315    {
316        self.depends = new.map(|x| x.into_iter().map(Into::into).collect());
317    }
318
319    /// Get the due date of the task
320    pub fn due(&self) -> Option<&Date> {
321        self.due.as_ref()
322    }
323
324    /// Get the due date of the task mutable
325    pub fn due_mut(&mut self) -> Option<&mut Date> {
326        self.due.as_mut()
327    }
328
329    /// Set due
330    pub fn set_due<T>(&mut self, new: Option<T>)
331    where
332        T: Into<Date>,
333    {
334        self.due = new.map(Into::into)
335    }
336
337    /// Get the end date of the task
338    pub fn end(&self) -> Option<&Date> {
339        self.end.as_ref()
340    }
341
342    /// Get the end date of the task mutable
343    pub fn end_mut(&mut self) -> Option<&mut Date> {
344        self.end.as_mut()
345    }
346
347    /// Set end
348    pub fn set_end<T>(&mut self, new: Option<T>)
349    where
350        T: Into<Date>,
351    {
352        self.end = new.map(Into::into)
353    }
354
355    /// Get the imask of the task
356    pub fn imask(&self) -> Option<&f64> {
357        self.imask.as_ref()
358    }
359
360    /// Get the imask of the task mutable
361    pub fn imask_mut(&mut self) -> Option<&mut f64> {
362        self.imask.as_mut()
363    }
364
365    /// Set imask
366    pub fn set_imask<T>(&mut self, new: Option<T>)
367    where
368        T: Into<f64>,
369    {
370        self.imask = new.map(Into::into)
371    }
372
373    /// Get the mask of the task
374    pub fn mask(&self) -> Option<&String> {
375        self.mask.as_ref()
376    }
377
378    /// Get the mask of the task mutable
379    pub fn mask_mut(&mut self) -> Option<&mut String> {
380        self.mask.as_mut()
381    }
382
383    /// Set mask
384    pub fn set_mask<T>(&mut self, new: Option<T>)
385    where
386        T: Into<String>,
387    {
388        self.mask = new.map(Into::into)
389    }
390
391    /// Get the modified date of the task
392    pub fn modified(&self) -> Option<&Date> {
393        self.modified.as_ref()
394    }
395
396    /// Get the modified date of the task mutable
397    pub fn modified_mut(&mut self) -> Option<&mut Date> {
398        self.modified.as_mut()
399    }
400
401    /// Set modified
402    pub fn set_modified<T>(&mut self, new: Option<T>)
403    where
404        T: Into<Date>,
405    {
406        self.modified = new.map(Into::into)
407    }
408
409    /// Get the parent of the task
410    pub fn parent(&self) -> Option<&Uuid> {
411        self.parent.as_ref()
412    }
413
414    /// Get the parent of the task mutable
415    pub fn parent_mut(&mut self) -> Option<&mut Uuid> {
416        self.parent.as_mut()
417    }
418
419    /// Set parent
420    pub fn set_parent<T>(&mut self, new: Option<T>)
421    where
422        T: Into<Uuid>,
423    {
424        self.parent = new.map(Into::into)
425    }
426
427    /// Get the priority of the task
428    pub fn priority(&self) -> Option<&TaskPriority> {
429        self.priority.as_ref()
430    }
431
432    /// Get the priority of the task mutable
433    pub fn priority_mut(&mut self) -> Option<&mut TaskPriority> {
434        self.priority.as_mut()
435    }
436
437    /// Set priority
438    pub fn set_priority<T>(&mut self, new: Option<T>)
439    where
440        T: Into<TaskPriority>,
441    {
442        self.priority = new.map(Into::into)
443    }
444
445    /// Get the project of the task
446    pub fn project(&self) -> Option<&Project> {
447        self.project.as_ref()
448    }
449
450    /// Get the project of the task mutable
451    pub fn project_mut(&mut self) -> Option<&mut Project> {
452        self.project.as_mut()
453    }
454
455    /// Set project
456    pub fn set_project<T>(&mut self, new: Option<T>)
457    where
458        T: Into<Project>,
459    {
460        self.project = new.map(Into::into)
461    }
462
463    /// Get the recur of the task
464    ///
465    /// This is exported as String by now. This might change in future versions of this crate.
466    pub fn recur(&self) -> Option<&String> {
467        self.recur.as_ref()
468    }
469
470    /// This is exported as String by now. This might change in future versions of this crate.
471    /// mutable
472    pub fn recur_mut(&mut self) -> Option<&mut String> {
473        self.recur.as_mut()
474    }
475
476    /// Set recur
477    pub fn set_recur<T>(&mut self, new: Option<T>)
478    where
479        T: Into<String>,
480    {
481        self.recur = new.map(Into::into)
482    }
483
484    /// Get the scheduled date of the task
485    pub fn scheduled(&self) -> Option<&Date> {
486        self.scheduled.as_ref()
487    }
488
489    /// Get the scheduled date of the task mutable
490    pub fn scheduled_mut(&mut self) -> Option<&mut Date> {
491        self.scheduled.as_mut()
492    }
493
494    /// Set scheduled
495    pub fn set_scheduled<T>(&mut self, new: Option<T>)
496    where
497        T: Into<Date>,
498    {
499        self.scheduled = new.map(Into::into)
500    }
501
502    /// Get the start date of the task
503    pub fn start(&self) -> Option<&Date> {
504        self.start.as_ref()
505    }
506
507    /// Get the start date of the task mutable
508    pub fn start_mut(&mut self) -> Option<&mut Date> {
509        self.start.as_mut()
510    }
511
512    /// Set start
513    pub fn set_start<T>(&mut self, new: Option<T>)
514    where
515        T: Into<Date>,
516    {
517        self.start = new.map(Into::into)
518    }
519
520    /// Get the tags of the task
521    pub fn tags(&self) -> Option<&Vec<Tag>> {
522        self.tags.as_ref()
523    }
524
525    /// Get the tags of the task mutable
526    pub fn tags_mut(&mut self) -> Option<&mut Vec<Tag>> {
527        self.tags.as_mut()
528    }
529
530    /// Set tags
531    pub fn set_tags<T>(&mut self, new: Option<T>)
532    where
533        T: IntoIterator,
534        T::Item: Into<Tag>,
535    {
536        self.tags = new.map(|x| x.into_iter().map(Into::into).collect());
537    }
538
539    /// Get the until date of the task
540    pub fn until(&self) -> Option<&Date> {
541        self.until.as_ref()
542    }
543
544    /// Get the until date of the task mutable
545    pub fn until_mut(&mut self) -> Option<&mut Date> {
546        self.until.as_mut()
547    }
548
549    /// Set until
550    pub fn set_until<T>(&mut self, new: Option<T>)
551    where
552        T: Into<Date>,
553    {
554        self.until = new.map(Into::into);
555    }
556
557    /// Get the urgency of the task
558    pub fn urgency(&self) -> Option<&Urgency> {
559        self.urgency.as_ref()
560    }
561
562    /// Get the urgency of the task
563    pub fn urgency_mut(&mut self) -> Option<&mut Urgency> {
564        self.urgency.as_mut()
565    }
566
567    /// Set the urgency of the task
568    pub fn set_urgency<T>(&mut self, new: Option<T>)
569    where
570        T: Into<Urgency>,
571    {
572        self.urgency = new.map(Into::into);
573    }
574
575    /// Get the wait date of the task
576    pub fn wait(&self) -> Option<&Date> {
577        self.wait.as_ref()
578    }
579
580    /// Get the wait date of the task mutable
581    pub fn wait_mut(&mut self) -> Option<&mut Date> {
582        self.wait.as_mut()
583    }
584
585    /// Set wait
586    pub fn set_wait<T>(&mut self, new: Option<T>)
587    where
588        T: Into<Date>,
589    {
590        self.wait = new.map(Into::into);
591    }
592
593    /// Get the BTreeMap that contains the UDA
594    pub fn uda(&self) -> &UDA {
595        &self.uda
596    }
597    /// Get the BTreeMap that contains the UDA mutable
598    pub fn uda_mut(&mut self) -> &mut UDA {
599        &mut self.uda
600    }
601}
602
603fn serialize_depends<S, T: 'static>(
604    field: &Option<Vec<Uuid>>,
605    serializer: S,
606) -> RResult<S::Ok, S::Error>
607where
608    S: Serializer,
609{
610    if std::any::TypeId::of::<T>() == std::any::TypeId::of::<TW25>() {
611        let value = field.as_ref().unwrap();
612        let v: Vec<String> = value.iter().map(Uuid::to_string).collect();
613        serializer.serialize_str(&v.join(","))
614    } else {
615        field.serialize(serializer)
616    }
617}
618
619fn deserialize_depends<'de, D, T: 'static>(deserializer: D) -> RResult<Option<Vec<Uuid>>, D::Error>
620where
621    D: Deserializer<'de>,
622{
623    if std::any::TypeId::of::<T>() == std::any::TypeId::of::<TW25>() {
624        let raw: String = String::deserialize(deserializer)?;
625        let mut uuids = vec![];
626        for uuid in raw.split(',') {
627            uuids.push(Uuid::parse_str(uuid).map_err(de::Error::custom)?);
628        }
629        Ok(Some(uuids))
630    } else {
631        let value: Option<Vec<Uuid>> = Option::deserialize(deserializer)?;
632        Ok(value)
633    }
634}
635
636#[cfg(test)]
637mod test {
638    use crate::annotation::Annotation;
639    use crate::date::Date;
640    use crate::date::TASKWARRIOR_DATETIME_TEMPLATE;
641    use crate::status::TaskStatus;
642    use crate::task::{Task, TW25, TW26};
643    use crate::uda::UDAValue;
644
645    use chrono::NaiveDateTime;
646    use serde_json;
647    use uuid::{uuid, Uuid};
648
649    fn mklogger() {
650        env_logger::init();
651        log::debug!("Env-logger enabled");
652    }
653
654    fn mkdate(s: &str) -> Date {
655        let n = NaiveDateTime::parse_from_str(s, TASKWARRIOR_DATETIME_TEMPLATE);
656        Date::from(n.unwrap())
657    }
658
659    #[test]
660    fn test_deser() {
661        let s = r#"{
662"id": 1,
663"description": "test",
664"entry": "20150619T165438Z",
665"status": "waiting",
666"uuid": "8ca953d5-18b4-4eb9-bd56-18f2e5b752f0",
667"urgency": 5.3
668}"#;
669
670        println!("{}", s);
671
672        let task = serde_json::from_str(s);
673        println!("{:?}", task);
674        assert!(task.is_ok());
675        let task: Task = task.unwrap();
676
677        assert_eq!(*task.status(), TaskStatus::Waiting);
678        assert_eq!(task.description(), "test");
679        assert_eq!(*task.entry(), mkdate("20150619T165438Z"));
680        assert_eq!(
681            *task.uuid(),
682            Uuid::parse_str("8ca953d5-18b4-4eb9-bd56-18f2e5b752f0").unwrap()
683        );
684        assert_eq!(task.urgency(), Some(&5.3));
685
686        let back = serde_json::to_string(&task).unwrap();
687
688        assert!(back.contains("description"));
689        assert!(back.contains("test"));
690        assert!(back.contains("entry"));
691        assert!(back.contains("20150619T165438Z"));
692        assert!(back.contains("status"));
693        assert!(back.contains("waiting"));
694        assert!(back.contains("uuid"));
695        assert!(back.contains("urgency"));
696        assert!(back.contains("8ca953d5-18b4-4eb9-bd56-18f2e5b752f0"));
697    }
698
699    #[test]
700    fn test_deser_more_tw26() {
701        let s = r#"{
702"id": 1,
703"description": "some description",
704"entry": "20150619T165438Z",
705"modified": "20160327T164007Z",
706"project": "someproject",
707"status": "waiting",
708"tags": ["some", "tags", "are", "here"],
709"uuid": "8ca953d5-18b4-4eb9-bd56-18f2e5b752f0",
710"depends": ["8ca953d5-18b4-4eb9-bd56-18f2e5b752f0","5a04bb1e-3f4b-49fb-b9ba-44407ca223b5"],
711"wait": "20160508T164007Z",
712"urgency": 0.583562
713}"#;
714        let task = serde_json::from_str(s);
715        assert!(task.is_ok());
716        let task: Task = task.unwrap();
717
718        assert_eq!(*task.status(), TaskStatus::Waiting);
719        assert_eq!(task.description(), "some description");
720        assert_eq!(*task.entry(), mkdate("20150619T165438Z"));
721        assert_eq!(
722            *task.uuid(),
723            Uuid::parse_str("8ca953d5-18b4-4eb9-bd56-18f2e5b752f0").unwrap()
724        );
725        assert_eq!(task.urgency(), Some(&0.583562));
726        assert_eq!(task.modified(), Some(&mkdate("20160327T164007Z")));
727        assert_eq!(task.project(), Some(&String::from("someproject")));
728
729        if let Some(tags) = task.tags() {
730            for tag in tags {
731                let any_tag = ["some", "tags", "are", "here"].iter().any(|t| tag == *t);
732                assert!(any_tag, "Tag {} missing", tag);
733            }
734        } else {
735            panic!("Tags completely missing");
736        }
737
738        assert_eq!(task.wait(), Some(&mkdate("20160508T164007Z")));
739
740        if let Some(depends) = task.depends() {
741            assert_eq!(depends.len(), 2);
742            assert!(depends.contains(&uuid!("8ca953d5-18b4-4eb9-bd56-18f2e5b752f0")));
743            assert!(depends.contains(&uuid!("5a04bb1e-3f4b-49fb-b9ba-44407ca223b5")));
744        } else {
745            panic!("Depends completely missing");
746        }
747
748        assert_eq!(task.wait(), Some(&mkdate("20160508T164007Z")));
749
750        let back = serde_json::to_string(&task).unwrap();
751
752        assert!(back.contains("description"));
753        assert!(back.contains("some description"));
754        assert!(back.contains("entry"));
755        assert!(back.contains("20150619T165438Z"));
756        assert!(back.contains("project"));
757        assert!(back.contains("someproject"));
758        assert!(back.contains("status"));
759        assert!(back.contains("waiting"));
760        assert!(back.contains("tags"));
761        assert!(back.contains("some"));
762        assert!(back.contains("tags"));
763        assert!(back.contains("are"));
764        assert!(back.contains("here"));
765        assert!(back.contains("uuid"));
766        assert!(back.contains("8ca953d5-18b4-4eb9-bd56-18f2e5b752f0"));
767        assert!(back.contains(
768            r#"["8ca953d5-18b4-4eb9-bd56-18f2e5b752f0","5a04bb1e-3f4b-49fb-b9ba-44407ca223b5"]"#,
769        ));
770    }
771
772    #[test]
773    fn test_deser_more_tw25() {
774        mklogger();
775        let s = r#"{
776"id": 1,
777"description": "some description",
778"entry": "20150619T165438Z",
779"modified": "20160327T164007Z",
780"project": "someproject",
781"status": "waiting",
782"tags": ["some", "tags", "are", "here"],
783"uuid": "8ca953d5-18b4-4eb9-bd56-18f2e5b752f0",
784"depends": "8ca953d5-18b4-4eb9-bd56-18f2e5b752f0,5a04bb1e-3f4b-49fb-b9ba-44407ca223b5",
785"wait": "20160508T164007Z",
786"urgency": 0.583562
787}"#;
788
789        println!("{}", s);
790
791        let task = serde_json::from_str(s);
792        println!("{:?}", task);
793        assert!(task.is_ok());
794        let task: Task<TW25> = task.unwrap();
795
796        assert_eq!(*task.status(), TaskStatus::Waiting);
797        assert_eq!(task.description(), "some description");
798        assert_eq!(*task.entry(), mkdate("20150619T165438Z"));
799        assert_eq!(
800            *task.uuid(),
801            Uuid::parse_str("8ca953d5-18b4-4eb9-bd56-18f2e5b752f0").unwrap()
802        );
803        assert_eq!(task.urgency(), Some(&0.583562));
804        assert_eq!(task.modified(), Some(&mkdate("20160327T164007Z")));
805        assert_eq!(task.project(), Some(&String::from("someproject")));
806
807        if let Some(tags) = task.tags() {
808            for tag in tags {
809                let any_tag = ["some", "tags", "are", "here"].iter().any(|t| tag == *t);
810                assert!(any_tag, "Tag {} missing", tag);
811            }
812        } else {
813            panic!("Tags completely missing");
814        }
815
816        if let Some(depends) = task.depends() {
817            assert_eq!(depends.len(), 2);
818            assert!(depends.contains(&uuid!("8ca953d5-18b4-4eb9-bd56-18f2e5b752f0")));
819            assert!(depends.contains(&uuid!("5a04bb1e-3f4b-49fb-b9ba-44407ca223b5")));
820        } else {
821            panic!("Depends completely missing");
822        }
823
824        assert_eq!(task.wait(), Some(&mkdate("20160508T164007Z")));
825
826        let back = serde_json::to_string(&task).unwrap();
827
828        assert!(back.contains("description"));
829        assert!(back.contains("some description"));
830        assert!(back.contains("entry"));
831        assert!(back.contains("20150619T165438Z"));
832        assert!(back.contains("project"));
833        assert!(back.contains("someproject"));
834        assert!(back.contains("status"));
835        assert!(back.contains("waiting"));
836        assert!(back.contains("tags"));
837        assert!(back.contains("some"));
838        assert!(back.contains("tags"));
839        assert!(back.contains("are"));
840        assert!(back.contains("here"));
841        assert!(back.contains("uuid"));
842        assert!(back.contains("8ca953d5-18b4-4eb9-bd56-18f2e5b752f0"));
843        assert!(
844            back.contains(
845                "8ca953d5-18b4-4eb9-bd56-18f2e5b752f0,5a04bb1e-3f4b-49fb-b9ba-44407ca223b5",
846            )
847        );
848    }
849
850    #[test]
851    fn test_deser_annotation() {
852        let s = r#"{
853"id":192,
854"description":"Some long description for a task",
855"entry":"20160423T125820Z",
856"modified":"20160423T125942Z",
857"project":"project",
858"status":"pending",
859"tags":["search","things"],
860"uuid":"5a04bb1e-3f4b-49fb-b9ba-44407ca223b5",
861"annotations":[{"entry":"20160423T125911Z","description":"An Annotation"},
862               {"entry":"20160423T125926Z","description":"Another Annotation"},
863               {"entry":"20160422T125942Z","description":"A Third Anno"}
864               ],
865"urgency": -5
866}"#;
867
868        println!("{}", s);
869
870        let task = serde_json::from_str(s);
871        println!("{:?}", task);
872        assert!(task.is_ok());
873        let task: Task = task.unwrap();
874
875        assert_eq!(task.urgency(), Some(&-5.0));
876
877        let all_annotations = vec![
878            Annotation::new(mkdate("20160423T125911Z"), String::from("An Annotation")),
879            Annotation::new(
880                mkdate("20160423T125926Z"),
881                String::from("Another Annotation"),
882            ),
883            Annotation::new(mkdate("20160422T125942Z"), String::from("A Third Anno")),
884        ];
885
886        if let Some(annotations) = task.annotations() {
887            for annotation in annotations {
888                let r = all_annotations.iter().any(|ann| {
889                    let descr = ann.description() == annotation.description();
890                    let entry = ann.entry() == annotation.entry();
891
892                    descr && entry
893                });
894                assert!(r, "Annotation {:?} missing or buggy", annotation);
895            }
896        } else {
897            panic!("Annotations missing");
898        }
899    }
900    #[test]
901    fn test_uda() {
902        let s = r#"{
903"description":"Some long description for a task",
904"entry":"20160423T125820Z",
905"modified":"20160423T125942Z",
906"project":"project",
907"status":"pending",
908"uuid":"5a04bb1e-3f4b-49fb-b9ba-44407ca223b5",
909"test_str_uda":"test_str_uda_value",
910"test_float_uda":-17.1234,
911"test_int_uda":1234
912}"#;
913
914        println!("{}", s);
915
916        let task = serde_json::from_str(s);
917        println!("{:?}", task);
918        assert!(task.is_ok());
919        let task: Task<TW25> = task.unwrap();
920
921        let str_uda = task.uda().get(&"test_str_uda".to_owned());
922        assert!(str_uda.is_some());
923        let str_uda = str_uda.unwrap();
924        assert_eq!(str_uda, &UDAValue::Str("test_str_uda_value".to_owned()));
925
926        let float_uda = task.uda().get(&"test_float_uda".to_owned());
927        assert!(float_uda.is_some());
928        let float_uda = float_uda.unwrap();
929        assert_eq!(float_uda, &UDAValue::F64(-17.1234));
930
931        let int_uda = task.uda().get(&"test_int_uda".to_owned());
932        assert!(int_uda.is_some());
933        let int_uda = int_uda.unwrap();
934        assert_eq!(int_uda, &UDAValue::U64(1234));
935
936        let back = serde_json::to_string_pretty(&task);
937        assert!(back.is_ok());
938        let back = back.unwrap();
939        println!("{}", back);
940        assert!(back.contains("description"));
941        assert!(back.contains("Some long description for a task"));
942        assert!(back.contains("entry"));
943        assert!(back.contains("20160423T125820Z"));
944        assert!(back.contains("project"));
945        assert!(back.contains("status"));
946        assert!(back.contains("pending"));
947        assert!(back.contains("uuid"));
948        assert!(back.contains("5a04bb1e-3f4b-49fb-b9ba-44407ca223b5"));
949        assert!(back.contains("test_str_uda"));
950        assert!(back.contains("test_str_uda_value"));
951        assert!(back.contains("test_float_uda"));
952        assert!(back.contains("-17.1234"));
953        assert!(back.contains("test_int_uda"));
954        assert!(back.contains("1234"));
955    }
956    #[test]
957    fn test_priority() {
958        let s = r#"{
959"id":9,
960"description":"Some long description for a task",
961"entry":"20201021T065503Z",
962"estimate":"30",
963"modified":"20210213T233603Z",
964"priority":"U",
965"status":"pending",
966"uuid":"6c4c9ee8-d6c4-4d64-a84d-bf9cb710684e",
967"urgency":23
968}"#;
969
970        println!("{}", s);
971
972        let task = serde_json::from_str(s);
973        println!("{:?}", task);
974        assert!(task.is_ok());
975        let task: Task = task.unwrap();
976
977        if let Some(priority) = task.priority() {
978            assert_eq!(*priority, "U".to_string());
979        } else {
980            panic!("Priority completely missing");
981        }
982
983        let back = serde_json::to_string_pretty(&task);
984        assert!(back.is_ok());
985        let back = back.unwrap();
986        println!("{}", back);
987        assert!(back.contains("description"));
988        assert!(back.contains("Some long description for a task"));
989        assert!(back.contains("entry"));
990        assert!(back.contains("20201021T065503Z"));
991        assert!(back.contains("priority"));
992        assert!(back.contains("status"));
993        assert!(back.contains("pending"));
994        assert!(back.contains("uuid"));
995        assert!(back.contains("6c4c9ee8-d6c4-4d64-a84d-bf9cb710684e"));
996    }
997
998    #[test]
999    fn test_builder_simple() {
1000        use crate::task::TaskBuilder;
1001
1002        let t = TaskBuilder::<TW25>::default()
1003            .description("test")
1004            .entry(mkdate("20150619T165438Z"))
1005            .build();
1006        println!("{:?}", t);
1007        assert!(t.is_ok());
1008        let t = t.unwrap();
1009
1010        assert_eq!(t.status(), &TaskStatus::Pending);
1011        assert_eq!(t.description(), "test");
1012        assert_eq!(t.entry(), &mkdate("20150619T165438Z"));
1013    }
1014    #[test]
1015    fn test_builder_extensive() {
1016        use crate::task::TaskBuilder;
1017        use crate::task::TW25;
1018        use crate::uda::{UDAValue, UDA};
1019        let mut uda = UDA::new();
1020        uda.insert(
1021            "test_str_uda".into(),
1022            UDAValue::Str("test_str_uda_value".into()),
1023        );
1024        uda.insert("test_int_uda".into(), UDAValue::U64(1234));
1025        uda.insert("test_float_uda".into(), UDAValue::F64(-17.1234));
1026        let t = TaskBuilder::<TW25>::default()
1027            .description("test")
1028            .entry(mkdate("20150619T165438Z"))
1029            .id(192)
1030            .modified(mkdate("20160423T125942Z"))
1031            .project("project".to_owned())
1032            .tags(vec!["search".to_owned(), "things".to_owned()])
1033            .uda(uda)
1034            .build();
1035        println!("{:?}", t);
1036        assert!(t.is_ok());
1037        let t = t.unwrap();
1038
1039        assert!(t.id().is_some());
1040        assert_eq!(t.id().unwrap(), 192);
1041        assert_eq!(t.status(), &TaskStatus::Pending);
1042        assert_eq!(t.description(), "test");
1043        assert_eq!(t.entry(), &mkdate("20150619T165438Z"));
1044        assert!(t.modified().is_some());
1045        assert_eq!(t.modified().unwrap(), &mkdate("20160423T125942Z"));
1046        assert!(t.project().is_some());
1047        assert_eq!(t.project().unwrap(), "project");
1048        assert!(t.tags().is_some());
1049        assert!(t.urgency().is_none());
1050        assert_eq!(
1051            t.tags().unwrap(),
1052            &vec!["search".to_owned(), "things".to_owned()]
1053        );
1054    }
1055    #[test]
1056    fn test_builder_defaults() {
1057        use crate::task::TaskBuilder;
1058        assert!(TaskBuilder::<TW25>::default()
1059            .description("Nice Task")
1060            .build()
1061            .is_ok());
1062    }
1063
1064    #[test]
1065    fn test_builder_fail() {
1066        use crate::task::TaskBuilder;
1067        assert!(TaskBuilder::<TW25>::default().build().is_err());
1068    }
1069
1070    const FIELD_NAMES_TO_NOT_SERIALIZE: [&str; 20] = [
1071        r#""id":"#,
1072        r#"""annotations:""#,
1073        r#""depends:""#,
1074        r#""due:""#,
1075        r#""end:""#,
1076        r#""imask:""#,
1077        r#""mask:""#,
1078        r#""modified:""#,
1079        r#""parent:""#,
1080        r#""priority:""#,
1081        r#""project:""#,
1082        r#""recur:""#,
1083        r#""scheduled:""#,
1084        r#""start:""#,
1085        r#""tags:""#,
1086        r#""until:""#,
1087        r#""wait:""#,
1088        r#""urgency:""#,
1089        r#""uda:""#,
1090        r#""_version:""#,
1091    ];
1092
1093    #[test]
1094    fn test_null_fields_not_serialized_tw25() {
1095        use crate::task::TaskBuilder;
1096
1097        let task = TaskBuilder::<TW25>::default()
1098            .description("Test Task")
1099            .build()
1100            .expect("Task to be built");
1101
1102        let task_as_str = serde_json::to_string_pretty(&task).expect("Task serialized as string");
1103
1104        for field_name in FIELD_NAMES_TO_NOT_SERIALIZE {
1105            assert!(
1106                !task_as_str.contains(field_name),
1107                "'{}' should not have been in {}",
1108                field_name,
1109                task_as_str
1110            );
1111        }
1112    }
1113
1114    #[test]
1115    fn test_null_fields_not_serialized_tw26() {
1116        use crate::task::TaskBuilder;
1117
1118        let task = TaskBuilder::<TW26>::default()
1119            .description("Test Task")
1120            .build()
1121            .expect("Task to be built");
1122
1123        let task_as_str = serde_json::to_string_pretty(&task).expect("Task serialized as string");
1124
1125        for field_name in FIELD_NAMES_TO_NOT_SERIALIZE {
1126            assert!(
1127                !task_as_str.contains(field_name),
1128                "'{}' should not have been in {}",
1129                field_name,
1130                task_as_str
1131            );
1132        }
1133    }
1134}