~matthiasbeyer/imag

57039654ca388d2d7a24dfdc05ec3de9ea113629 — Matthias Beyer 2 years ago 1047a06 + 0d50270
Merge branch 'libimagentrytag-refactor' into master
M bin/core/imag-tag/Cargo.toml => bin/core/imag-tag/Cargo.toml +1 -1
@@ 36,7 36,7 @@ default-features = false
features = ["color", "suggestions", "wrap_help"]

[dev-dependencies]
toml-query = "0.9"
toml-query = "0.9.2"
env_logger = "0.5"

[dev-dependencies.libimagutil]

M lib/entry/libimagentrytag/Cargo.toml => lib/entry/libimagentrytag/Cargo.toml +10 -2
@@ 24,16 24,24 @@ log = "0.4.0"
regex = "1"
toml = "0.5"
itertools = "0.7"
is-match = "0.1"
filters = "0.3"
toml-query = "0.9"
failure    = "0.1"
serde        = "1"
serde_derive = "1"

libimagstore = { version = "0.10.0", path = "../../../lib/core/libimagstore" }
libimagerror = { version = "0.10.0", path = "../../../lib/core/libimagerror" }

[dependencies.toml-query]
version = "0.9.2"
default-features = false
features = [ "typed" ]

[dependencies.clap]
version = "^2.29"
default-features = false
features = ["color", "suggestions", "wrap_help"]

[dev-dependencies]
env_logger = "0.5"


M lib/entry/libimagentrytag/src/lib.rs => lib/entry/libimagentrytag/src/lib.rs +4 -1
@@ 43,10 43,13 @@ extern crate itertools;
extern crate regex;
extern crate toml;
extern crate toml_query;
#[macro_use] extern crate is_match;
extern crate serde;
#[macro_use] extern crate serde_derive;
extern crate filters;
#[macro_use] extern crate failure;

#[cfg(test)] extern crate env_logger;

extern crate libimagstore;
extern crate libimagerror;


M lib/entry/libimagentrytag/src/tag.rs => lib/entry/libimagentrytag/src/tag.rs +4 -3
@@ 20,16 20,17 @@
use std::result::Result;

use regex::Regex;
use failure::Error;

pub type Tag = String;
pub type TagSlice<'a> = &'a str;

/// validator which can be used by clap to validate that a string is a valid tag
pub fn is_tag(s: String) -> Result<(), String> {
    is_tag_str(&s)
    is_tag_str(&s).map_err(|_| format!("The string '{}' is not a valid tag", s))
}

pub fn is_tag_str(s: &String) -> Result<(), String> {
pub fn is_tag_str(s: &String) -> Result<(), Error> {
    use filters::filter::Filter;
    trace!("Checking whether '{}' is a valid tag", s);



@@ 41,7 42,7 @@ pub fn is_tag_str(s: &String) -> Result<(), String> {
    if is_lower.and(no_whitespace).and(is_alphanum).and(matches_regex).filter(s) {
        Ok(())
    } else {
        Err(format!("The string '{}' is not a valid tag", s))
        Err(format_err!("The string '{}' is not a valid tag", s))
    }
}


M lib/entry/libimagentrytag/src/tagable.rs => lib/entry/libimagentrytag/src/tagable.rs +140 -107
@@ 23,17 23,14 @@ use libimagstore::store::Entry;
use libimagerror::errors::ErrorMsg as EM;

use toml_query::read::TomlValueReadExt;
use toml_query::read::Partial;
use toml_query::insert::TomlValueInsertExt;

use failure::Error;
use failure::ResultExt;
use failure::Fallible as Result;
use failure::err_msg;
use crate::tag::{Tag, TagSlice};
use crate::tag::is_tag_str;

use toml::Value;

pub trait Tagable {

    fn get_tags(&self) -> Result<Vec<Tag>>;


@@ 47,144 44,180 @@ pub trait Tagable {

}

impl Tagable for Value {
#[derive(Serialize, Deserialize, Debug)]
struct TagHeader {
    values: Vec<String>,
}

impl<'a> Partial<'a> for TagHeader {
    const LOCATION: &'static str = "tag";
    type Output                  = Self;
}

impl Tagable for Entry {

    fn get_tags(&self) -> Result<Vec<Tag>> {
        self.read("tag.values")
            .context(format_err!("Failed to read header at 'tag.values'"))
            .map_err(Error::from)
            .context(EM::EntryHeaderReadError)?
            .map(|val| {
                debug!("Got Value of tags...");
                val.as_array()
                    .map(|tags| {
                        debug!("Got Array<T> of tags...");
                        if !tags.iter().all(|t| is_match!(*t, Value::String(_))) {
                            return Err(format_err!("Tag type error: Got Array<T> where T is not a String: {:?}", tags));
                        }
                        debug!("Got Array<String> of tags...");
                        if tags.iter().any(|t| match *t {
                            Value::String(ref s) => !is_tag_str(s).is_ok(),
                            _ => unreachable!()})
                        {
                            return Err(format_err!("At least one tag is not a valid tag string"));
                        }

                        Ok(tags.iter()
                            .cloned()
                            .map(|t| {
                                match t {
                                   Value::String(s) => s,
                                   _ => unreachable!(),
                                }
                            })
                            .collect())
                    })
                    .unwrap_or(Ok(vec![]))
        self.get_header()
            .read_partial::<TagHeader>()?
            .map(|header| {
                let _ = header.values
                    .iter()
                    .map(is_tag_str)
                    .collect::<Result<_>>()?;

                Ok(header.values)
            })
            .unwrap_or(Ok(vec![]))
            .unwrap_or_else(|| Ok(vec![]))
    }

    fn set_tags(&mut self, ts: &[Tag]) -> Result<()> {
        if ts.iter().any(|tag| !is_tag_str(tag).is_ok()) {
            let not_tag = ts.iter().filter(|t| !is_tag_str(t).is_ok()).next().unwrap();
            return Err(format_err!("Not a tag: '{}'", not_tag));
        }

        let a = ts.iter().unique().map(|t| Value::String(t.clone())).collect();
        debug!("Setting tags = {:?}", a);
        self.insert("tag.values", Value::Array(a))
        let _ = ts
            .iter()
            .map(is_tag_str)
            .collect::<Result<Vec<_>>>()?;

        let header = TagHeader {
            values: ts.iter().unique().cloned().collect(),
        };

        debug!("Setting tags = {:?}", header);
        self.get_header_mut()
            .insert_serialized("tag", header)
            .map(|_| ())
            .map_err(|_| Error::from(EM::EntryHeaderWriteError))
    }

    fn add_tag(&mut self, t: Tag) -> Result<()> {
        if !is_tag_str(&t).map(|_| true)
            .map_err(|s| format_err!("{}", s))
            .context(err_msg("Not a tag"))?
        {
            return Err(format_err!("Not a tag: '{}'", t));
        }

        self.get_tags()
            .map(|mut tags| {
                debug!("Pushing tag = {:?} to list = {:?}", t, tags);
                tags.push(t);
                self.set_tags(&tags.into_iter().unique().collect::<Vec<_>>()[..])
            })
            .map(|_| ())
        let _ = is_tag_str(&t)?;

        let mut tags = self.get_tags()?;
        debug!("Pushing tag = {:?} to list = {:?}", t, tags);
        tags.push(t);
        self.set_tags(&tags)
    }

    fn remove_tag(&mut self, t: Tag) -> Result<()> {
        if !is_tag_str(&t).map(|_| true)
            .map_err(|s| format_err!("{}", s))
            .context(err_msg("Not a tag"))?
        {
            debug!("Not a tag: '{}'", t);
            return Err(format_err!("Not a tag: '{}'", t));
        }

        self.get_tags()
            .map(|mut tags| {
                tags.retain(|tag| tag.clone() != t);
                self.set_tags(&tags[..])
            })
            .map(|_| ())
        let _ = is_tag_str(&t)?;

        let mut tags = self.get_tags()?;
        tags.retain(|tag| *tag != t);
        self.set_tags(&tags)
    }

    fn has_tag(&self, t: TagSlice) -> Result<bool> {
        let tags = self.read("tag.values").context(EM::EntryHeaderReadError)?;

        if !tags.iter().all(|t| is_match!(*t, &Value::String(_))) {
            return Err(err_msg("Tag type error"))
        }

        Ok(tags
           .iter()
           .any(|tag| {
               match *tag {
                   &Value::String(ref s) => { s == t },
                   _ => unreachable!()
               }
           }))
        // use any() because Vec::contains() wants &String, but we do not want to allocate.
        self.get_tags().map(|v| v.iter().any(|s| s == t))
    }

    fn has_tags(&self, tags: &[Tag]) -> Result<bool> {
        let mut result = true;
        for tag in tags {
            result = result && self.has_tag(tag)?;
        }

        Ok(result)
        tags.iter().map(|t| self.has_tag(t)).fold(Ok(true), |a, e| a.and_then(|b| Ok(b && e?)))
    }


}

impl Tagable for Entry {
#[cfg(test)]
mod tests {
    use std::path::PathBuf;

    fn get_tags(&self) -> Result<Vec<Tag>> {
        self.get_header().get_tags()
    use toml_query::read::TomlValueReadTypeExt;

    use libimagstore::store::Store;

    use super::*;

    fn setup_logging() {
        let _ = ::env_logger::try_init();
    }

    fn set_tags(&mut self, ts: &[Tag]) -> Result<()> {
        self.get_header_mut().set_tags(ts)
    fn get_store() -> Store {
        Store::new_inmemory(PathBuf::from("/"), &None).unwrap()
    }

    fn add_tag(&mut self, t: Tag) -> Result<()> {
        self.get_header_mut().add_tag(t)
    #[test]
    fn test_tag_get_tag() {
        setup_logging();
        let store = get_store();
        let name = "test-tag-get-tags";

        debug!("Creating default entry");
        let id = PathBuf::from(String::from(name));
        let mut entry = store.create(id).unwrap();

        let tags = vec![String::from("a")];
        entry.set_tags(&tags).unwrap();

        let v = entry.get_tags();

        assert!(v.is_ok());
        let v = v.unwrap();

        assert_eq!(v, vec!["a"]);
    }

    fn remove_tag(&mut self, t: Tag) -> Result<()> {
        self.get_header_mut().remove_tag(t)
    #[test]
    fn test_tag_add_adds_tag() {
        setup_logging();
        let store = get_store();
        let name = "test-tag-set-sets-tags";

        debug!("Creating default entry");
        let id = PathBuf::from(String::from(name));
        let mut entry = store.create(id).unwrap();

        entry.add_tag(String::from("test")).unwrap();

        let v = entry.get_header().read_string("tag.values.[0]").unwrap();

        assert!(v.is_some());
        let v = v.unwrap();

        assert_eq!(v, "test");
    }

    fn has_tag(&self, t: TagSlice) -> Result<bool> {
        self.get_header().has_tag(t)
    #[test]
    fn test_tag_remove_removes_tag() {
        setup_logging();
        let store = get_store();
        let name = "test-tag-set-sets-tags";

        debug!("Creating default entry");
        let id = PathBuf::from(String::from(name));
        let mut entry = store.create(id).unwrap();

        entry.add_tag(String::from("test")).unwrap();

        let v = entry.get_header().read_string("tag.values.[0]").unwrap();
        assert!(v.is_some());

        entry.remove_tag(String::from("test")).unwrap();

        assert!(entry.get_header().read_string("tag.values.[0]").is_err());
        let tags = entry.get_tags();
        assert!(tags.is_ok());
        let tags = tags.unwrap();
        assert!(tags.is_empty());
    }

    fn has_tags(&self, ts: &[Tag]) -> Result<bool> {
        self.get_header().has_tags(ts)
    #[test]
    fn test_tag_set_sets_tag() {
        setup_logging();
        let store = get_store();
        let name = "test-tag-set-sets-tags";

        debug!("Creating default entry");
        let id = PathBuf::from(String::from(name));
        let mut entry = store.create(id).unwrap();

        let tags = vec![String::from("testtag")];
        entry.set_tags(&tags).unwrap();

        let v = entry.get_header().read_string("tag.values.[0]").unwrap();

        assert!(v.is_some());
        let v = v.unwrap();

        assert_eq!(v, "testtag");
    }

}