libimagtagcmd/
lib.rs

1//
2// imag - the personal information management suite for the commandline
3// Copyright (C) 2015-2020 Matthias Beyer <mail@beyermatthias.de> and contributors
4//
5// This library is free software; you can redistribute it and/or
6// modify it under the terms of the GNU Lesser General Public
7// License as published by the Free Software Foundation; version
8// 2.1 of the License.
9//
10// This library is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13// Lesser General Public License for more details.
14//
15// You should have received a copy of the GNU Lesser General Public
16// License along with this library; if not, write to the Free Software
17// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
18//
19
20#![forbid(unsafe_code)]
21
22#![deny(
23    non_camel_case_types,
24    non_snake_case,
25    path_statements,
26    trivial_numeric_casts,
27    unstable_features,
28    unused_allocation,
29    unused_import_braces,
30    unused_imports,
31    unused_must_use,
32    unused_mut,
33    unused_qualifications,
34    while_true,
35)]
36
37extern crate clap;
38extern crate resiter;
39#[macro_use] extern crate log;
40
41#[cfg(test)] extern crate toml;
42#[macro_use] extern crate failure;
43
44extern crate libimagstore;
45extern crate libimagrt;
46extern crate libimagentrytag;
47extern crate libimagerror;
48
49#[cfg(test)]
50#[macro_use]
51extern crate libimagutil;
52
53#[cfg(not(test))]
54extern crate libimagutil;
55
56#[cfg(test)]
57extern crate toml_query;
58
59#[cfg(test)]
60extern crate env_logger;
61
62use std::io::Write;
63
64use failure::Fallible as Result;
65use failure::err_msg;
66use resiter::AndThen;
67use resiter::Map;
68use resiter::FilterMap;
69
70use libimagrt::runtime::Runtime;
71use libimagrt::application::ImagApplication;
72use libimagentrytag::tagable::Tagable;
73use libimagentrytag::tag::is_tag_str;
74use libimagentrytag::tag::Tag;
75use libimagstore::storeid::StoreId;
76
77use clap::{App, ArgMatches};
78
79mod ui;
80
81
82/// Marker enum for implementing ImagApplication on
83///
84/// This is used by binaries crates to execute business logic
85/// or to build a CLI completion.
86pub enum ImagTag {}
87impl ImagApplication for ImagTag {
88    fn run(rt: Runtime) -> Result<()> {
89        let process = |iter: &mut dyn Iterator<Item = Result<StoreId>>| -> Result<()> {
90            match rt.cli().subcommand() {
91                ("list", _) => iter
92                    .map_ok(|id| list(id, &rt, true))
93                    .collect::<Result<Vec<_>>>()
94                    .map(|_| ()),
95
96                ("remove", _) => iter.and_then_ok(|id| {
97                    let add = None;
98                    let rem = get_remove_tags(rt.cli())?;
99                    debug!("id = {:?}, add = {:?}, rem = {:?}", id, add, rem);
100                    alter(&rt, id, add, rem)
101                }).collect(),
102
103                ("add", _) => iter.and_then_ok(|id| {
104                    let add = get_add_tags(rt.cli())?;
105                    let rem = None;
106                    debug!("id = {:?}, add = {:?}, rem = {:?}", id, add, rem);
107                    alter(&rt, id, add, rem)
108                }).collect(),
109
110                ("present", Some(scmd)) => {
111                    let must_be_present = scmd
112                        .values_of("present-tag")
113                        .unwrap()
114                        .map(String::from)
115                        .collect::<Vec<String>>();
116
117                    must_be_present.iter().map(|t| is_tag_str(t)).collect::<Result<Vec<_>>>()?;
118
119                    iter.filter_map_ok(|id| {
120                            match rt.store().get(id.clone()) {
121                                Err(e) => Some(Err(e)),
122                                Ok(None) => Some(Err(format_err!("No entry for id {}", id))),
123                                Ok(Some(entry)) => {
124                                    let entry_tags = match entry.get_tags() {
125                                        Err(e) => return Some(Err(e)),
126                                        Ok(e) => e,
127                                    };
128
129                                    if must_be_present.iter().all(|pres| entry_tags.contains(pres)) {
130                                        Some(Ok(entry))
131                                    } else {
132                                        None
133                                    }
134                                }
135                            }
136                        })
137                        .flatten()
138                        .and_then_ok(|e| {
139                            if !rt.output_is_pipe() {
140                                writeln!(rt.stdout(), "{}", e.get_location())?;
141                            }
142                            Ok(e)
143                        })
144                        .and_then_ok(|e| rt.report_touched(e.get_location()))
145                        .collect::<Result<Vec<_>>>()
146                        .map(|_| ())
147                },
148
149                ("missing", Some(scmd)) => {
150                    let must_be_missing = scmd
151                        .values_of("missing-tag")
152                        .unwrap()
153                        .map(String::from)
154                        .collect::<Vec<String>>();
155
156                    must_be_missing.iter().map(|t| is_tag_str(t)).collect::<Result<Vec<_>>>()?;
157
158                    iter.filter_map_ok(|id| {
159                            match rt.store().get(id.clone()) {
160                                Err(e) => Some(Err(e)),
161                                Ok(None) => Some(Err(format_err!("No entry for id {}", id))),
162                                Ok(Some(entry)) => {
163                                    let entry_tags = match entry.get_tags() {
164                                        Err(e) => return Some(Err(e)),
165                                        Ok(e) => e,
166                                    };
167
168                                    if must_be_missing.iter().all(|miss| !entry_tags.contains(miss)) {
169                                        Some(Ok(entry))
170                                    } else {
171                                        None
172                                    }
173                                }
174                            }
175                        })
176                        .flatten()
177                        .and_then_ok(|e| {
178                            if !rt.output_is_pipe() {
179                                writeln!(rt.stdout(), "{}", e.get_location())?;
180                            }
181                            Ok(e)
182                        })
183                        .and_then_ok(|e| rt.report_touched(e.get_location()))
184                        .collect::<Result<Vec<_>>>()
185                        .map(|_| ())
186                },
187
188                (_, None) => {
189                    debug!("No subcommand, using 'list'");
190                    iter.map_ok(|id| list(id, &rt, false))
191                        .collect::<Result<Vec<_>>>()
192                        .map(|_| ())
193                },
194
195                (other, _) => {
196                    debug!("Unknown command");
197                    if rt.handle_unknown_subcommand("imag-tag", other, rt.cli())?.success() {
198                        Ok(())
199                    } else {
200                        Err(format_err!("Subcommand failed"))
201                    }
202                },
203            }
204        };
205
206        match rt.ids::<crate::ui::PathProvider>()? {
207            Some(ids) => process(&mut ids.into_iter().map(Ok)),
208            None => process(&mut rt.store().entries()?),
209        }
210    }
211
212    fn build_cli<'a>(app: App<'a, 'a>) -> App<'a, 'a> {
213        ui::build_ui(app)
214    }
215
216    fn name() -> &'static str {
217        env!("CARGO_PKG_NAME")
218    }
219
220    fn description() -> &'static str {
221        "Manage tags of entries"
222    }
223
224    fn version() -> &'static str {
225        env!("CARGO_PKG_VERSION")
226    }
227}
228
229fn alter(rt: &Runtime, path: StoreId, add: Option<Vec<Tag>>, rem: Option<Vec<Tag>>) -> Result<()> {
230    match rt.store().get(path.clone())? {
231        Some(mut e) => {
232            debug!("Entry header now = {:?}", e.get_header());
233
234            if let Some(tags) = add {
235                debug!("Adding tags = '{:?}'", tags);
236                tags.into_iter().map(|tag| {
237                    debug!("Adding tag '{:?}'", tag);
238                    e.add_tag(tag)
239                }).collect::<Result<Vec<_>>>()?;
240            } // it is okay to ignore a None here
241
242            debug!("Entry header now = {:?}", e.get_header());
243
244            if let Some(tags) = rem {
245                debug!("Removing tags = '{:?}'", tags);
246                tags.into_iter().map(|tag| {
247                    debug!("Removing tag '{:?}'", tag);
248                    e.remove_tag(tag)
249                }).collect::<Result<Vec<_>>>()?;
250            } // it is okay to ignore a None here
251
252            debug!("Entry header now = {:?}", e.get_header());
253        },
254
255        None => {
256            info!("No entry found.");
257        },
258    }
259
260    rt.report_touched(&path)
261}
262
263fn list(path: StoreId, rt: &Runtime, has_subcommand: bool) -> Result<()> {
264    let entry        = rt.store().get(path.clone())?.ok_or_else(|| err_msg("No entry found"))?;
265    let (scmd, json_out, line_out, sepp_out, mut comm_out) = if has_subcommand {
266        let scmd     = rt.cli().subcommand_matches("list").unwrap();
267        let json_out = scmd.is_present("json");
268        let line_out = scmd.is_present("linewise");
269        let sepp_out = scmd.is_present("sep");
270        let comm_out = scmd.is_present("commasep");
271
272        (Some(scmd), json_out, line_out, sepp_out, comm_out)
273    } else {
274        (None, false, false, false, false)
275    };
276
277    if !vec![json_out, line_out, comm_out, sepp_out].iter().any(|v| *v) {
278        // None of the flags passed, go to default
279        comm_out = true;
280    }
281
282    let tags = entry.get_tags()?;
283
284    if json_out {
285        unimplemented!()
286    }
287
288    if line_out {
289        for tag in &tags {
290            writeln!(rt.stdout(), "{}", tag)?;
291        }
292    }
293
294    if sepp_out {
295        let sepp = scmd.map(|s| s.value_of("sep").unwrap()).unwrap_or("");
296        writeln!(rt.stdout(), "{}", tags.join(sepp))?;
297    }
298
299    if comm_out {
300        writeln!(rt.stdout(), "{}", tags.join(", "))?;
301    }
302
303    rt.report_touched(&path)
304}
305
306/// Get the tags which should be added from the commandline
307///
308/// Returns none if the argument was not specified
309fn get_add_tags(matches: &ArgMatches) -> Result<Option<Vec<Tag>>> {
310    retrieve_tags(matches, "add", "add-tags")
311}
312
313/// Get the tags which should be removed from the commandline
314///
315/// Returns none if the argument was not specified
316fn get_remove_tags(matches: &ArgMatches) -> Result<Option<Vec<Tag>>> {
317    retrieve_tags(matches, "remove", "remove-tags")
318}
319
320fn retrieve_tags(m: &ArgMatches, s: &'static str, v: &'static str) -> Result<Option<Vec<Tag>>> {
321    Ok(Some(m
322         .subcommand_matches(s)
323         .ok_or_else(|| format_err!("Expected subcommand '{}', but was not specified", s))?
324         .values_of(v)
325         .unwrap() // enforced by clap
326         .map(String::from)
327         .collect()))
328}
329
330#[cfg(test)]
331mod tests {
332    use std::path::PathBuf;
333    use std::ffi::OsStr;
334
335    use toml::value::Value;
336    use toml_query::read::TomlValueReadExt;
337    use failure::Fallible as Result;
338    use failure::Error;
339
340    use libimagrt::runtime::Runtime;
341    use libimagstore::storeid::StoreId;
342    use libimagstore::store::{FileLockEntry, Entry};
343
344    use super::*;
345
346    make_mock_app! {
347        app "imag-tag";
348        modulename mock;
349        version env!("CARGO_PKG_VERSION");
350        with help "imag-tag mocking app";
351        with ui builder function crate::ui::build_ui;
352    }
353    use self::mock::generate_test_runtime;
354
355    fn create_test_default_entry<'a, S: AsRef<OsStr>>(rt: &'a Runtime, name: S) -> Result<StoreId> {
356        let mut path = PathBuf::new();
357        path.set_file_name(name);
358
359        let default_entry = Entry::new(StoreId::new(PathBuf::from("")).unwrap())
360            .to_str()
361            .unwrap();
362
363        let id = StoreId::new(path)?;
364        let mut entry = rt.store().create(id.clone())?;
365        entry.get_content_mut().push_str(&default_entry);
366
367        Ok(id)
368    }
369
370    fn get_entry_tags<'a>(entry: &'a FileLockEntry<'a>) -> Result<Option<&'a Value>> {
371        entry.get_header().read(&"tag.values".to_owned()).map_err(Error::from)
372    }
373
374    fn tags_toml_value<I: IntoIterator<Item = &'static str>>(tags: I) -> Value {
375        Value::Array(tags.into_iter().map(|s| Value::String(s.to_owned())).collect())
376    }
377
378    fn setup_logging() {
379        let _ = ::env_logger::try_init();
380    }
381
382    #[test]
383    fn test_tag_add_adds_tag() -> Result<()> {
384        setup_logging();
385        debug!("Generating runtime");
386        let name = "test-tag-add-adds-tags";
387        let rt = generate_test_runtime(vec![name, "add", "foo"]).unwrap();
388
389        debug!("Creating default entry");
390        create_test_default_entry(&rt, name).unwrap();
391        let id = PathBuf::from(String::from(name));
392
393        debug!("Getting 'add' tags");
394        let add = get_add_tags(rt.cli())?;
395        debug!("Add-tags: {:?}", add);
396
397        debug!("Altering things");
398        alter(&rt, StoreId::new(id.clone()).unwrap(), add, None)?;
399        debug!("Altered");
400
401        let test_entry = rt.store().get(id).unwrap().unwrap();
402
403        let test_tags  = get_entry_tags(&test_entry);
404        assert!(test_tags.is_ok(), "Should be Ok(_) = {:?}", test_tags);
405
406        let test_tags  = test_tags.unwrap();
407        assert!(test_tags.is_some(), "Should be Some(_) = {:?}", test_tags);
408        let test_tags  = test_tags.unwrap();
409
410        assert_ne!(*test_tags, tags_toml_value(vec![]));
411        assert_eq!(*test_tags, tags_toml_value(vec!["foo"]));
412        Ok(())
413    }
414
415    #[test]
416    fn test_tag_remove_removes_tag() -> Result<()> {
417        setup_logging();
418        debug!("Generating runtime");
419        let name = "test-tag-remove-removes-tag";
420        let rt = generate_test_runtime(vec![name, "remove", "foo"]).unwrap();
421
422        debug!("Creating default entry");
423        create_test_default_entry(&rt, name).unwrap();
424        let id = PathBuf::from(String::from(name));
425
426        // Manually add tags
427        let add = Some(vec![ "foo".to_owned() ]);
428
429        debug!("Getting 'remove' tags");
430        let rem = get_remove_tags(rt.cli())?;
431        debug!("Rem-tags: {:?}", rem);
432
433        debug!("Altering things");
434        alter(&rt, StoreId::new(id.clone()).unwrap(), add, rem)?;
435        debug!("Altered");
436
437        let test_entry = rt.store().get(id).unwrap().unwrap();
438        let test_tags  = get_entry_tags(&test_entry).unwrap().unwrap();
439
440        assert_eq!(*test_tags, tags_toml_value(vec![]));
441        Ok(())
442    }
443
444    #[test]
445    fn test_tag_remove_removes_only_to_remove_tag() -> Result<()> {
446        setup_logging();
447        debug!("Generating runtime");
448        let name = "test-tag-remove-removes-only-to-remove-tag-doesnt-crash-on-nonexistent-tag";
449        let rt = generate_test_runtime(vec![name, "remove", "foo"]).unwrap();
450
451        debug!("Creating default entry");
452        create_test_default_entry(&rt, name).unwrap();
453        let id = PathBuf::from(String::from(name));
454
455        // Manually add tags
456        let add = Some(vec![ "foo".to_owned(), "bar".to_owned() ]);
457
458        debug!("Getting 'remove' tags");
459        let rem = get_remove_tags(rt.cli())?;
460        debug!("Rem-tags: {:?}", rem);
461
462        debug!("Altering things");
463        alter(&rt, StoreId::new(id.clone()).unwrap(), add, rem)?;
464        debug!("Altered");
465
466        let test_entry = rt.store().get(id).unwrap().unwrap();
467        let test_tags  = get_entry_tags(&test_entry).unwrap().unwrap();
468
469        assert_eq!(*test_tags, tags_toml_value(vec!["bar"]));
470        Ok(())
471    }
472
473    #[test]
474    fn test_tag_remove_removes_but_doesnt_crash_on_nonexistent_tag() -> Result<()> {
475        setup_logging();
476        debug!("Generating runtime");
477        let name = "test-tag-remove-removes-but-doesnt-crash-on-nonexistent-tag";
478        let rt = generate_test_runtime(vec![name, "remove", "foo", "bar"]).unwrap();
479
480        debug!("Creating default entry");
481        create_test_default_entry(&rt, name).unwrap();
482        let id = PathBuf::from(String::from(name));
483
484        // Manually add tags
485        let add = Some(vec![ "foo".to_owned() ]);
486
487        debug!("Getting 'remove' tags");
488        let rem = get_remove_tags(rt.cli())?;
489        debug!("Rem-tags: {:?}", rem);
490
491        debug!("Altering things");
492        alter(&rt, StoreId::new(id.clone()).unwrap(), add, rem)?;
493        debug!("Altered");
494
495        let test_entry = rt.store().get(id).unwrap().unwrap();
496        let test_tags  = get_entry_tags(&test_entry).unwrap().unwrap();
497
498        assert_eq!(*test_tags, tags_toml_value(vec![]));
499        Ok(())
500    }
501
502}
503