Merge pull request #174 from matthiasbeyer/libimagtag/init

Libimagtag/init
This commit is contained in:
Matthias Beyer 2016-02-28 17:58:15 +01:00
commit 8ba96591c2
8 changed files with 345 additions and 0 deletions

14
libimagtag/Cargo.toml Normal file
View file

@ -0,0 +1,14 @@
[package]
name = "libimagtag"
version = "0.1.0"
authors = ["Matthias Beyer <mail@beyermatthias.de>"]
[dependencies]
clap = "2.1.1"
log = "0.3.5"
regex = "0.1.47"
toml = "0.1.25"
[dependencies.libimagstore]
path = "../libimagstore"

70
libimagtag/src/error.rs Normal file
View file

@ -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<Box<Error>>,
}
impl TagError {
pub fn new(errtype: TagErrorKind, cause: Option<Box<Error>>) -> 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)
}
}

14
libimagtag/src/lib.rs Normal file
View file

@ -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;

6
libimagtag/src/result.rs Normal file
View file

@ -0,0 +1,6 @@
use std::result::Result as RResult;
use error::TagError;
pub type Result<T> = RResult<T, TagError>;

1
libimagtag/src/tag.rs Normal file
View file

@ -0,0 +1 @@
pub type Tag = String;

165
libimagtag/src/tagable.rs Normal file
View file

@ -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<Vec<Tag>>;
fn set_tags(&mut self, ts: Vec<Tag>) -> Result<()>;
fn add_tag(&mut self, t: Tag) -> Result<()>;
fn remove_tag(&mut self, t: Tag) -> Result<()>;
fn has_tag(&self, t: &Tag) -> Result<bool>;
fn has_tags(&self, ts: &Vec<Tag>) -> Result<bool>;
}
impl Tagable for EntryHeader {
fn get_tags(&self) -> Result<Vec<Tag>> {
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<Tag>) -> 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<bool> {
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<Tag>) -> Result<bool> {
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<Vec<Tag>> {
self.get_header().get_tags()
}
fn set_tags(&mut self, ts: Vec<Tag>) -> 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<bool> {
self.get_header().has_tag(t)
}
fn has_tags(&self, ts: &Vec<Tag>) -> Result<bool> {
self.get_header().has_tags(ts)
}
}

70
libimagtag/src/ui.rs Normal file
View file

@ -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 <mail@beyermatthias.de>")
.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<Vec<Tag>> {
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<Vec<Tag>> {
extract_tags(matches, "remove-tags", '-')
}
fn extract_tags(matches: &ArgMatches, specifier: &str, specchar: char) -> Option<Vec<Tag>> {
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()
})
}
}

5
libimagtag/src/util.rs Normal file
View file

@ -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()
}