1#![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
82pub 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 } 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 } 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 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
306fn get_add_tags(matches: &ArgMatches) -> Result<Option<Vec<Tag>>> {
310 retrieve_tags(matches, "add", "add-tags")
311}
312
313fn 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() .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 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 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 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