diff --git a/libimagtag/Cargo.toml b/libimagtag/Cargo.toml new file mode 100644 index 00000000..0fd71402 --- /dev/null +++ b/libimagtag/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "libimagtag" +version = "0.1.0" +authors = ["Matthias Beyer "] + +[dependencies] +clap = "2.1.1" +log = "0.3.5" +regex = "0.1.47" +toml = "0.1.25" + +[dependencies.libimagstore] +path = "../libimagstore" + diff --git a/libimagtag/src/error.rs b/libimagtag/src/error.rs new file mode 100644 index 00000000..ed15e283 --- /dev/null +++ b/libimagtag/src/error.rs @@ -0,0 +1,70 @@ +use std::error::Error; +use std::fmt::Error as FmtError; +use std::clone::Clone; +use std::fmt::{Debug, Display, Formatter}; +use std::fmt; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum TagErrorKind { + TagTypeError, + HeaderReadError, + HeaderWriteError, + NotATag, +} + +fn tag_error_type_as_str(e: &TagErrorKind) -> &'static str { + match e { + &TagErrorKind::TagTypeError => "Entry Header Tag Type wrong", + &TagErrorKind::HeaderReadError => "Error while reading entry header", + &TagErrorKind::HeaderWriteError => "Error while writing entry header", + &TagErrorKind::NotATag => "String is not a tag", + } +} + +impl Display for TagErrorKind { + + fn fmt(&self, fmt: &mut Formatter) -> Result<(), FmtError> { + try!(write!(fmt, "{}", tag_error_type_as_str(self))); + Ok(()) + } + +} + +#[derive(Debug)] +pub struct TagError { + kind: TagErrorKind, + cause: Option>, +} + +impl TagError { + + pub fn new(errtype: TagErrorKind, cause: Option>) -> TagError { + TagError { + kind: errtype, + cause: cause, + } + } + +} + +impl Display for TagError { + + fn fmt(&self, fmt: &mut Formatter) -> Result<(), FmtError> { + try!(write!(fmt, "[{}]", tag_error_type_as_str(&self.kind.clone()))); + Ok(()) + } + +} + +impl Error for TagError { + + fn description(&self) -> &str { + tag_error_type_as_str(&self.kind.clone()) + } + + fn cause(&self) -> Option<&Error> { + self.cause.as_ref().map(|e| &**e) + } + +} + diff --git a/libimagtag/src/lib.rs b/libimagtag/src/lib.rs new file mode 100644 index 00000000..f4d1c543 --- /dev/null +++ b/libimagtag/src/lib.rs @@ -0,0 +1,14 @@ +extern crate clap; +#[macro_use] extern crate log; +extern crate regex; +extern crate toml; + +extern crate libimagstore; + +pub mod error; +pub mod result; +pub mod tag; +pub mod tagable; +pub mod util; +pub mod ui; + diff --git a/libimagtag/src/result.rs b/libimagtag/src/result.rs new file mode 100644 index 00000000..a01166ca --- /dev/null +++ b/libimagtag/src/result.rs @@ -0,0 +1,6 @@ +use std::result::Result as RResult; + +use error::TagError; + +pub type Result = RResult; + diff --git a/libimagtag/src/tag.rs b/libimagtag/src/tag.rs new file mode 100644 index 00000000..cba2322e --- /dev/null +++ b/libimagtag/src/tag.rs @@ -0,0 +1 @@ +pub type Tag = String; diff --git a/libimagtag/src/tagable.rs b/libimagtag/src/tagable.rs new file mode 100644 index 00000000..f38f49d6 --- /dev/null +++ b/libimagtag/src/tagable.rs @@ -0,0 +1,165 @@ +use libimagstore::store::{Entry, EntryHeader}; + +use error::{TagError, TagErrorKind}; +use result::Result; +use tag::Tag; +use util::is_tag; + +use toml::Value; + +pub trait Tagable { + + fn get_tags(&self) -> Result>; + fn set_tags(&mut self, ts: Vec) -> Result<()>; + + fn add_tag(&mut self, t: Tag) -> Result<()>; + fn remove_tag(&mut self, t: Tag) -> Result<()>; + + fn has_tag(&self, t: &Tag) -> Result; + fn has_tags(&self, ts: &Vec) -> Result; + +} + +impl Tagable for EntryHeader { + + fn get_tags(&self) -> Result> { + let tags = self.read("imag.tags"); + if tags.is_err() { + let kind = TagErrorKind::HeaderReadError; + return Err(TagError::new(kind, Some(Box::new(tags.err().unwrap())))); + } + let tags = tags.unwrap(); + + match tags { + Some(Value::Array(tags)) => { + if !tags.iter().all(|t| match t { &Value::String(_) => true, _ => false }) { + return Err(TagError::new(TagErrorKind::TagTypeError, None)); + } + if tags.iter().any(|t| match t { + &Value::String(ref s) => !is_tag(&s), + _ => unreachable!()}) + { + return Err(TagError::new(TagErrorKind::NotATag, None)); + } + + Ok(tags.iter() + .cloned() + .map(|t| { + match t { + Value::String(s) => s, + _ => unreachable!(), + } + }) + .collect()) + }, + None => Ok(vec![]), + _ => Err(TagError::new(TagErrorKind::TagTypeError, None)), + } + } + + fn set_tags(&mut self, ts: Vec) -> Result<()> { + if ts.iter().any(|tag| !is_tag(tag)) { + debug!("Not a tag: '{}'", ts.iter().filter(|t| !is_tag(t)).next().unwrap()); + return Err(TagError::new(TagErrorKind::NotATag, None)); + } + + let a = ts.iter().map(|t| Value::String(t.clone())).collect(); + self.set("imag.tags", Value::Array(a)) + .map(|_| ()) + .map_err(|e| TagError::new(TagErrorKind::HeaderWriteError, Some(Box::new(e)))) + } + + fn add_tag(&mut self, t: Tag) -> Result<()> { + if !is_tag(&t) { + debug!("Not a tag: '{}'", t); + return Err(TagError::new(TagErrorKind::NotATag, None)); + } + + self.get_tags() + .map(|mut tags| { + tags.push(t); + self.set_tags(tags) + }) + .map(|_| ()) + } + + fn remove_tag(&mut self, t: Tag) -> Result<()> { + if !is_tag(&t) { + debug!("Not a tag: '{}'", t); + return Err(TagError::new(TagErrorKind::NotATag, None)); + } + + self.get_tags() + .map(|mut tags| { + tags.retain(|tag| tag.clone() != t); + self.set_tags(tags) + }) + .map(|_| ()) + } + + fn has_tag(&self, t: &Tag) -> Result { + let tags = self.read("imag.tags"); + if tags.is_err() { + let kind = TagErrorKind::HeaderReadError; + return Err(TagError::new(kind, Some(Box::new(tags.err().unwrap())))); + } + let tags = tags.unwrap(); + + if !tags.iter().all(|t| match t { &Value::String(_) => true, _ => false }) { + return Err(TagError::new(TagErrorKind::TagTypeError, None)); + } + + Ok(tags + .iter() + .any(|tag| { + match tag { + &Value::String(ref s) => { s == t }, + _ => unreachable!() + } + })) + } + + fn has_tags(&self, tags: &Vec) -> Result { + let mut result = true; + for tag in tags { + let check = self.has_tag(tag); + if check.is_err() { + return Err(check.err().unwrap()); + } + let check = check.unwrap(); + + result = result && check; + } + + Ok(result) + } + +} + +impl Tagable for Entry { + + fn get_tags(&self) -> Result> { + self.get_header().get_tags() + } + + fn set_tags(&mut self, ts: Vec) -> Result<()> { + self.get_header_mut().set_tags(ts) + } + + fn add_tag(&mut self, t: Tag) -> Result<()> { + self.get_header_mut().add_tag(t) + } + + fn remove_tag(&mut self, t: Tag) -> Result<()> { + self.get_header_mut().remove_tag(t) + } + + fn has_tag(&self, t: &Tag) -> Result { + self.get_header().has_tag(t) + } + + fn has_tags(&self, ts: &Vec) -> Result { + self.get_header().has_tags(ts) + } + +} diff --git a/libimagtag/src/ui.rs b/libimagtag/src/ui.rs new file mode 100644 index 00000000..e889e224 --- /dev/null +++ b/libimagtag/src/ui.rs @@ -0,0 +1,70 @@ +use clap::{Arg, ArgMatches, App, SubCommand}; + +use tag::Tag; + +/// Generates a clap::SubCommand to be integrated in the commandline-ui builder for building a +/// "tags --add foo --remove bar" subcommand to do tagging action. +pub fn tag_subcommand<'a, 'b>() -> App<'a, 'b> { + SubCommand::with_name("tags") + .author("Matthias Beyer ") + .version("0.1") + .about("Add or remove tags") + + .arg(Arg::with_name("add-tags") + .short("a") + .long("add") + .takes_value(true) + .multiple(true) + .help("Add tags, seperated by comma or by specifying multiple times")) + + .arg(Arg::with_name("remove-tags") + .short("r") + .long("remove") + .takes_value(true) + .multiple(true) + .help("Remove tags, seperated by comma or by specifying multiple times")) +} + +/// Generates a clap::Arg which can be integrated into the commandline-ui builder for building a +/// "-t" or "--tags" argument which takes values for tagging actions (add, remove) +pub fn tag_argument<'a, 'b>() -> Arg<'a, 'b> { + Arg::with_name("specify-tags") + .short("t") + .long("tags") + .takes_value(true) + .multiple(true) + .help("Add or remove tags, prefixed by '+' (for adding) or '-' (for removing)") +} + +/// Get the tags which should be added from the commandline +/// +/// Returns none if the argument was not specified +pub fn get_add_tags(matches: &ArgMatches) -> Option> { + extract_tags(matches, "add-tags", '+') +} + +/// Get the tags which should be removed from the commandline +/// +/// Returns none if the argument was not specified +pub fn get_remove_tags(matches: &ArgMatches) -> Option> { + extract_tags(matches, "remove-tags", '-') +} + +fn extract_tags(matches: &ArgMatches, specifier: &str, specchar: char) -> Option> { + if let Some(submatch) = matches.subcommand_matches("tags") { + submatch.values_of(specifier) + .map(|values| values.map(String::from).collect()) + } else { + matches.values_of("specify-tags") + .map(|argmatches| { + argmatches + .map(String::from) + .filter(|s| s.chars().next() == Some(specchar)) + .map(|s| { + String::from(s.split_at(1).1) + }) + .collect() + }) + } +} + diff --git a/libimagtag/src/util.rs b/libimagtag/src/util.rs new file mode 100644 index 00000000..caa32539 --- /dev/null +++ b/libimagtag/src/util.rs @@ -0,0 +1,5 @@ +use regex::Regex; + +pub fn is_tag(s: &String) -> bool { + Regex::new("^[a-zA-Z]([a-zA-Z0-9_-]*)$").unwrap().captures(&s[..]).is_some() +}