1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
//
// imag - the personal information management suite for the commandline
// Copyright (C) 2015-2018 Matthias Beyer <mail@beyermatthias.de> and contributors
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; version
// 2.1 of the License.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
//

use std::collections::BTreeMap;
use std::io::BufRead;
use std::result::Result as RResult;

use toml::Value;
use uuid::Uuid;

use task_hookrs::task::Task as TTask;
use task_hookrs::import::{import_task, import_tasks};
use failure::Fallible as Result;
use failure::ResultExt;
use failure::Error;
use failure::err_msg;

use libimagstore::store::{FileLockEntry, Store};
use libimagstore::storeid::IntoStoreId;
use libimagerror::errors::ErrorMsg as EM;
use module_path::ModuleEntryPath;

use iter::TaskIdIterator;

/// Task struct containing a `FileLockEntry`
pub trait TaskStore<'a> {
    fn import_task_from_reader<R: BufRead>(&'a self, r: R) -> Result<(FileLockEntry<'a>, String, Uuid)>;
    fn get_task_from_import<R: BufRead>(&'a self, r: R) -> Result<RResult<FileLockEntry<'a>, String>>;
    fn get_task_from_string(&'a self, s: String) -> Result<RResult<FileLockEntry<'a>, String>>;
    fn get_task_from_uuid(&'a self, uuid: Uuid) -> Result<Option<FileLockEntry<'a>>>;
    fn retrieve_task_from_import<R: BufRead>(&'a self, r: R) -> Result<FileLockEntry<'a>>;
    fn retrieve_task_from_string(&'a self, s: String) -> Result<FileLockEntry<'a>>;
    fn delete_tasks_by_imports<R: BufRead>(&self, r: R) -> Result<()>;
    fn delete_task_by_uuid(&self, uuid: Uuid) -> Result<()>;
    fn all_tasks(&self) -> Result<TaskIdIterator>;
    fn new_from_twtask(&'a self, task: TTask) -> Result<FileLockEntry<'a>>;
}

impl<'a> TaskStore<'a> for Store {

    fn import_task_from_reader<R: BufRead>(&'a self, mut r: R) -> Result<(FileLockEntry<'a>, String, Uuid)> {
        let mut line = String::new();
        r.read_line(&mut line).context(EM::UTF8Error)?;
        import_task(&line.as_str())
            .context(err_msg("Error importing"))
            .map_err(Error::from)
            .and_then(|t| {
                let uuid = t.uuid().clone();
                self.new_from_twtask(t).map(|t| (t, line, uuid))
            })
    }

    /// Get a task from an import string. That is: read the imported string, get the UUID from it
    /// and try to load this UUID from store.
    ///
    /// Possible return values are:
    ///
    /// * Ok(Ok(Task))
    /// * Ok(Err(String)) - where the String is the String read from the `r` parameter
    /// * Err(_)          - where the error is an error that happened during evaluation
    ///
    fn get_task_from_import<R: BufRead>(&'a self, mut r: R) -> Result<RResult<FileLockEntry<'a>, String>> {
        let mut line = String::new();
        r.read_line(&mut line).context(EM::UTF8Error)?;
        self.get_task_from_string(line)
    }

    /// Get a task from a String. The String is expected to contain the JSON-representation of the
    /// Task to get from the store (only the UUID really matters in this case)
    ///
    /// For an explanation on the return values see `Task::get_from_import()`.
    fn get_task_from_string(&'a self, s: String) -> Result<RResult<FileLockEntry<'a>, String>> {
        import_task(s.as_str())
            .context(err_msg("Import error"))
            .map_err(Error::from)
            .map(|t| t.uuid().clone())
            .and_then(|uuid| self.get_task_from_uuid(uuid))
            .and_then(|o| match o {
                None    => Ok(Err(s)),
                Some(t) => Ok(Ok(t)),
            })
    }

    /// Get a task from an UUID.
    ///
    /// If there is no task with this UUID, this returns `Ok(None)`.
    fn get_task_from_uuid(&'a self, uuid: Uuid) -> Result<Option<FileLockEntry<'a>>> {
        ModuleEntryPath::new(format!("taskwarrior/{}", uuid))
            .into_storeid()
            .and_then(|store_id| self.get(store_id))
    }

    /// Same as Task::get_from_import() but uses Store::retrieve() rather than Store::get(), to
    /// implicitely create the task if it does not exist.
    fn retrieve_task_from_import<R: BufRead>(&'a self, mut r: R) -> Result<FileLockEntry<'a>> {
        let mut line = String::new();
        r.read_line(&mut line).context(EM::UTF8Error)?;
        self.retrieve_task_from_string(line)
    }

    /// Retrieve a task from a String. The String is expected to contain the JSON-representation of
    /// the Task to retrieve from the store (only the UUID really matters in this case)
    fn retrieve_task_from_string(&'a self, s: String) -> Result<FileLockEntry<'a>> {
        self.get_task_from_string(s)
            .and_then(|opt| match opt {
                Ok(task)    => Ok(task),
                Err(string) => import_task(string.as_str())
                    .context(err_msg("Import error"))
                    .map_err(Error::from)
                    .and_then(|t| self.new_from_twtask(t)),
            })
    }

    fn delete_tasks_by_imports<R: BufRead>(&self, r: R) -> Result<()> {
        use serde_json::ser::to_string as serde_to_string;
        use task_hookrs::status::TaskStatus;

        for (counter, res_ttask) in import_tasks(r).into_iter().enumerate() {
            let ttask = res_ttask.context(err_msg("Import error"))?;

            if counter % 2 == 1 {
                // Only every second task is needed, the first one is the
                // task before the change, and the second one after
                // the change. The (maybe modified) second one is
                // expected by taskwarrior.
                let val = serde_to_string(&ttask).context(err_msg("Import error"))?;

                // Taskwarrior does not have the concept of deleted tasks, but only modified
                // ones.
                //
                // Here we check if the status of a task is deleted and if yes, we delete it
                // from the store.
                if *ttask.status() == TaskStatus::Deleted {
                    let _ = self.delete_task_by_uuid(*ttask.uuid())?;
                    info!("Deleted task {}", *ttask.uuid());
                }
            }
        }
        Ok(())
    }

    fn delete_task_by_uuid(&self, uuid: Uuid) -> Result<()> {
        ModuleEntryPath::new(format!("taskwarrior/{}", uuid))
            .into_storeid()
            .and_then(|id| self.delete(id))
    }

    fn all_tasks(&self) -> Result<TaskIdIterator> {
        self.entries().map(|i| TaskIdIterator::new(i.without_store()))
    }

    fn new_from_twtask(&'a self, task: TTask) -> Result<FileLockEntry<'a>> {
        use toml_query::read::TomlValueReadExt;
        use toml_query::set::TomlValueSetExt;

        let uuid     = task.uuid();
        ModuleEntryPath::new(format!("taskwarrior/{}", uuid))
            .into_storeid()
            .and_then(|id| {
                self.retrieve(id)
                    .and_then(|mut fle| {
                        {
                            let hdr = fle.get_header_mut();
                            if hdr.read("todo")?.is_none() {
                                hdr.set("todo", Value::Table(BTreeMap::new()))?;
                            }

                            hdr.set("todo.uuid", Value::String(format!("{}",uuid)))?;
                        }

                        // If none of the errors above have returned the function, everything is fine
                        Ok(fle)
                    })
            })

    }

}