tdo_core/
tdo.rs

1//! General implementation of tdos base structure.
2use json::parse;
3use std::fs::File;
4use std::io::{Read, Write, stdout, stdin};
5use list::TodoList;
6use legacy::*;
7use todo::Todo;
8use error::*;
9
10/// Basic container structure for a set of todo lists.
11///
12/// This data structure acts as a conatiner for all todo lists and its associated todos.
13/// The whole `tdo` microcosm settles around this structure
14/// which is also used for (de-)serialization.
15///
16/// When instanciated, it comes with an empty _default_ list.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct Tdo {
19    /// A vector of all todo lists.
20    pub lists: Vec<TodoList>,
21    //The Github API token.
22    access_token: Option<String>,
23    // The tdo version the last dump was saved with.
24    version: String,
25}
26
27impl Tdo {
28    /// Create a new `Tdo` container.
29    /// Each new container is instanciated with a _default_ `TodoList`.
30    ///
31    /// # Example
32    ///
33    /// ```
34    /// # use tdo_core::tdo::*;
35    /// let tdo = Tdo::new();
36    /// ```
37    pub fn new() -> Tdo {
38        Tdo {
39            lists: vec![TodoList::default()],
40            access_token: None,
41            version: env!("CARGO_PKG_VERSION").to_string(),
42        }
43    }
44
45    /// Load a saved `Tdo` container from a JSON file.
46    ///
47    /// This function returns a `ResultType` which will yield the
48    /// deserialized JSON or a `serde_json::Error`.
49    ///
50    /// # Example
51    ///
52    /// ```
53    /// # use tdo_core::tdo::*;
54    /// let mut tdo = Tdo::load("foo.json");
55    /// ```
56    pub fn load(path: &str) -> TdoResult<Tdo> {
57        match File::open(path) {
58            Ok(file) => {
59                match super::serde_json::from_reader(&file) {
60                    Ok(tdo) => Ok(tdo),
61                    Err(_) => update_json(path),
62                }
63            }
64            Err(_) => Err(ErrorKind::StorageError(storage_error::ErrorKind::FileNotFound).into()),
65        }
66
67    }
68
69    /// Dump the `Tdo` container to a JSON file.
70    ///
71    /// This function returns a `ResultType` yielding a `StorageError::SaveFailure`
72    /// if the JSON file could not be opened/saved.
73    ///
74    /// # Example
75    ///
76    /// ```
77    /// # use tdo_core::tdo::*;
78    /// # let mut tdo = Tdo::new();
79    /// let res = tdo.save("foo.json");
80    /// assert_eq!(res.unwrap(), ());
81    /// ```
82    pub fn save(&self, path: &str) -> TdoResult<()> {
83        // TODO: At this point we could be much more precise about the error if we would include
84        // the error from the file system as SaveFailure(ArbitraryErrorFromFS)
85        //  -- Feliix42 (2017-03-14; 17:04)
86        match File::create(path) {
87            Ok(mut f) => {
88                let _ = super::serde_json::to_writer_pretty(&mut f, self);
89                Ok(())
90            }
91            Err(_) => Err(ErrorKind::StorageError(storage_error::ErrorKind::SaveFailure).into()),
92        }
93    }
94
95    /// Sets the GitHub access token.
96    pub fn set_gh_token(&mut self, token: Option<&str>) {
97        let gh_token = match token {
98            Some(x) => x.to_string(),
99            None => {
100                print!("Please generate an access token ({})\nand enter a valid accesstoken: ",
101                       "https://github.com/settings/tokens/new?scopes=repo&description=tdolist");
102                stdout().flush().ok().expect("Could not flush stdout!");
103                let mut answer = String::new();
104                stdin().read_line(&mut answer).unwrap();
105                answer.trim().to_string()
106            }
107        };
108        self.access_token = Some(gh_token);
109    }
110
111    /// Returns an Option<String> of the private access_token field.
112    pub fn get_gh_token(&self) -> Option<String> {
113        self.access_token.to_owned()
114    }
115
116    /// Add a todo list to the container.
117    pub fn add_list(&mut self, list: TodoList) -> TdoResult<()> {
118        match self.get_list_index(&list.name) {
119            Ok(_) => Err(ErrorKind::TodoError(todo_error::ErrorKind::NameAlreadyExists).into()),
120            Err(_) => {
121                self.lists.push(list);
122                Ok(())
123            }
124        }
125    }
126
127    /// Removes a list from the container.
128    pub fn remove_list(&mut self, list_name: &str) -> TdoResult<()> {
129        if list_name == "default" {
130            Err(ErrorKind::TodoError(todo_error::ErrorKind::CanNotRemoveDefault).into())
131        } else {
132            match self.get_list_index(list_name) {
133                Ok(index) => {
134                    self.lists.remove(index);
135                    Ok(())
136                }
137                Err(_) => Err(ErrorKind::TodoError(todo_error::ErrorKind::NoSuchList).into()),
138            }
139        }
140    }
141
142    /// Add a todo to the todo list, identified by its name.
143    ///
144    /// This function returns a `ResultType` with a `TodoError::NoSuchList`
145    /// if there is no matching list found.
146    pub fn add_todo(&mut self, list_name: Option<&str>, todo: Todo) -> TdoResult<()> {
147        match self.get_list_index(&list_name.unwrap_or("default")) {
148            Ok(index) => {
149                self.lists[index].add(todo);
150                Ok(())
151            }
152            Err(x) => Err(x),
153        }
154    }
155
156    /// Cycle through all todo lists and find the list which contains the todo with the given ID
157    ///
158    /// This function retuns a `ResultType` with a `TodoError::NotInList`
159    /// if there is no list found or a usize with the postition of the list in lists.
160    pub fn find_id(&self, id: u32) -> TdoResult<usize> {
161        for list in 0..self.lists.len() {
162            if self.lists[list].contains_id(id).is_ok() {
163                return Ok(list);
164            }
165        }
166        Err(ErrorKind::TodoError(todo_error::ErrorKind::NotInList).into())
167    }
168    /// Cycle through all todo lists and mark a todo with the given ID as done.
169    /// This function has no return value and thus won't indicate whether
170    /// there was a matching todo found.
171    pub fn done_id(&mut self, id: u32) -> TdoResult<()> {
172        let list = match self.find_id(id) {
173            Ok(list_id) => list_id,
174            Err(e) => return Err(e),
175        };
176        self.lists[list].done_id(id)
177    }
178
179    /// Cycle through all todo lists and remove a todo with the given id.
180    /// This function has no return value and thus won't indicate whether
181    /// there was a matching todo found.
182    pub fn remove_id(&mut self, id: u32) -> TdoResult<()> {
183        let list = match self.find_id(id) {
184            Ok(list_id) => list_id,
185            Err(e) => return Err(e),
186        };
187        match self.lists[list].remove_id(id) {
188            Err(e) => Err(e),
189            _ => Ok(()),
190        }
191    }
192
193    /// Remove all todos that have been marked as _done_ from all todo lists.
194    pub fn clean_lists(&mut self) {
195        for list in 0..self.lists.len() {
196            self.lists[list].clean();
197        }
198    }
199
200    /// Remove all todos that have been marked as _done_ from a given todo list.
201    pub fn clean_list(&mut self, list: &str) -> TdoResult<()> {
202        let index = match self.get_list_index(list) {
203            Ok(index) => index,
204            Err(e) => return Err(e),
205        };
206        self.lists[index].clean();
207        Ok(())
208    }
209
210    fn get_list_index(&self, name: &str) -> TdoResult<usize> {
211        match self.lists
212            .iter()
213            .position(|x| x.name.to_lowercase() == name.to_string().to_lowercase()) {
214            Some(index) => Ok(index),
215            None => Err(ErrorKind::TodoError(todo_error::ErrorKind::NoSuchList).into()),
216        }
217    }
218
219    /// Get the highest ID used in the tdo container.
220    pub fn get_highest_id(&self) -> u32 {
221        self.lists
222            .iter()
223            .fold(0, |acc, &ref x| {
224                x.list
225                    .iter()
226                    .fold(acc,
227                          |inner_acc, &ref y| if inner_acc < y.id { y.id } else { inner_acc })
228            })
229    }
230
231    /// Move a `todo` between two lists.
232    pub fn move_todo(&mut self, id: u32, target_list: &str) -> TdoResult<()> {
233        let src_index = self.find_id(id)?;
234        let target = self.get_list_index(target_list)?;
235
236        //Check if todo is a github Issue
237        let list_index = self.lists[src_index].contains_id(id)?;
238        if let Some(_) = self.lists[src_index].list[list_index].github {
239            return Err(ErrorKind::GithubError(github_error::ErrorKind::NotAllowedToMove).into())
240        }
241        let todo = self.lists[src_index].pop_id(id)?;
242        self.lists[target].insert_todo(todo);
243        Ok(())
244    }
245}
246
247fn update_json(path: &str) -> TdoResult<Tdo> {
248    match Tdo01::load(path) {
249        Ok(tdo) => Ok(tdo.into()),
250        Err(_) => {
251            println!("I have to do this here");
252            let mut file = File::open(path).unwrap();
253            let mut data = String::new();
254            file.read_to_string(&mut data).unwrap();
255            let mut json = match parse(&data) {
256                Ok(content) => content,
257                Err(_) => {
258                    return Err(ErrorKind::StorageError(storage_error::ErrorKind::FileCorrupted)
259                        .into())
260                }
261            };
262
263            let mut lists: Vec<TodoList> = vec![];
264
265            for outer in json.entries_mut() {
266                let mut list = TodoList::new(outer.0);
267                for inner in outer.1.entries_mut() {
268                    let tdo_id = match inner.0.parse::<u32>() {
269                        Ok(id) => id,
270                        Err(_) => return Err(ErrorKind::StorageError(storage_error::ErrorKind::UnableToConvert).into()),
271                    };
272                    let done = match inner.1.pop().as_bool() {
273                        Some(x) => x,
274                        None => return Err(ErrorKind::StorageError(storage_error::ErrorKind::UnableToConvert).into()),
275                    };
276                    let tdo_name = match inner.1.pop().as_str() {
277                        Some(x) => String::from(x),
278                        None => return Err(ErrorKind::StorageError(storage_error::ErrorKind::UnableToConvert).into()),
279                    };
280                    let mut todo = Todo::new(tdo_id, &tdo_name, None);
281                    if done {
282                        todo.set_done();
283                    }
284                    list.add(todo);
285                }
286                lists.push(list);
287            }
288            let tdo = Tdo {
289                lists: lists,
290                access_token: None,
291                version: env!("CARGO_PKG_VERSION").to_string(),
292            };
293            Ok(tdo)
294        }
295    }
296}