Add support for directional links

This patch adds support for directional links in libimagentrylink.

A directional link is added to the same header as the "internal" links,
which are unidirectional.

The "outgoing" link is added in "links.to", the "incoming" links are
added to "links.from".

This way we can always traverse all linkage. The `Linkable::links()`
method is changed to always return _all_ link.
The `Link` type was extended for notions of "to" and "from" links.

Also adds testing.

Signed-off-by: Matthias Beyer <mail@beyermatthias.de>
This commit is contained in:
Matthias Beyer 2019-06-21 22:30:03 +02:00
parent 56f8aecdbf
commit 0eac6d9931
2 changed files with 193 additions and 5 deletions

View file

@ -30,6 +30,8 @@ use failure::Error;
#[derive(Eq, PartialOrd, Ord, Hash, Debug, Clone)]
pub enum Link {
Id { link: StoreId },
LinkTo { link: StoreId },
LinkFrom { link: StoreId },
}
impl Link {
@ -37,6 +39,8 @@ impl Link {
pub fn exists(&self, store: &Store) -> Result<bool> {
match *self {
Link::Id { ref link } => store.exists(link.clone()),
Link::LinkTo { ref link } => store.exists(link.clone()),
Link::LinkFrom { ref link } => store.exists(link.clone()),
}
.map_err(From::from)
}
@ -44,6 +48,8 @@ impl Link {
pub fn to_str(&self) -> Result<String> {
match *self {
Link::Id { ref link } => link.to_str(),
Link::LinkTo { ref link } => link.to_str(),
Link::LinkFrom { ref link } => link.to_str(),
}
.map_err(From::from)
}
@ -52,6 +58,8 @@ impl Link {
pub(crate) fn eq_store_id(&self, id: &StoreId) -> bool {
match self {
&Link::Id { link: ref s } => s.eq(id),
&Link::LinkTo { ref link } => link.eq(id),
&Link::LinkFrom { ref link } => link.eq(id),
}
}
@ -59,17 +67,21 @@ impl Link {
pub fn get_store_id(&self) -> &StoreId {
match self {
&Link::Id { link: ref s } => s,
&Link::LinkTo { ref link } => link,
&Link::LinkFrom { ref link } => link,
}
}
pub(crate) fn to_value(&self) -> Result<Value> {
match self {
Link::Id { ref link } => link
.to_str()
.map(Value::String)
.context(EM::ConversionError)
.map_err(Error::from),
Link::Id { ref link } => link,
Link::LinkTo { ref link } => link,
Link::LinkFrom { ref link } => link,
}
.to_str()
.map(Value::String)
.context(EM::ConversionError)
.map_err(Error::from)
}
}
@ -78,6 +90,9 @@ impl ::std::cmp::PartialEq for Link {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(&Link::Id { link: ref a }, &Link::Id { link: ref b }) => a.eq(&b),
(&Link::LinkTo { link: ref a }, &Link::LinkTo { link: ref b })=> a.eq(&b),
(&Link::LinkFrom { link: ref a }, &Link::LinkFrom { link: ref b })=> a.eq(&b),
_ => false,
}
}
}
@ -93,6 +108,8 @@ impl Into<StoreId> for Link {
fn into(self) -> StoreId {
match self {
Link::Id { link } => link,
Link::LinkTo { link } => link,
Link::LinkFrom { link } => link,
}
}
}
@ -101,6 +118,8 @@ impl IntoStoreId for Link {
fn into_storeid(self) -> Result<StoreId> {
match self {
Link::Id { link } => Ok(link),
Link::LinkTo { link } => Ok(link),
Link::LinkFrom { link } => Ok(link),
}
}
}
@ -109,6 +128,8 @@ impl AsRef<StoreId> for Link {
fn as_ref(&self) -> &StoreId {
match self {
&Link::Id { ref link } => &link,
&Link::LinkTo { ref link } => &link,
&Link::LinkFrom { ref link } => &link,
}
}
}

View file

@ -38,6 +38,15 @@ pub trait Linkable {
/// Get all links
fn links(&self) -> Result<LinkIter>;
/// Get all links which are unidirectional links
fn unidirectional_links(&self) -> Result<LinkIter>;
/// Get all links which are directional links, outgoing
fn directional_links_to(&self) -> Result<LinkIter>;
/// Get all links which are directional links, incoming
fn directional_links_from(&self) -> Result<LinkIter>;
/// Add an internal link to the implementor object
fn add_link(&mut self, link: &mut Entry) -> Result<()>;
@ -47,17 +56,27 @@ pub trait Linkable {
/// Remove _all_ internal links
fn unlink(&mut self, store: &Store) -> Result<()>;
/// Add a directional link: self -> otehr
fn add_link_to(&mut self, other: &mut Entry) -> Result<()>;
/// Remove a directional link: self -> otehr
fn remove_link_to(&mut self, other: &mut Entry) -> Result<()>;
}
#[derive(Serialize, Deserialize, Debug)]
struct LinkPartial {
internal: Option<Vec<String>>,
from: Option<Vec<String>>,
to: Option<Vec<String>>,
}
impl Default for LinkPartial {
fn default() -> Self {
LinkPartial {
internal: None,
from: None,
to: None,
}
}
}
@ -82,6 +101,8 @@ impl Linkable for Entry {
.internal
.unwrap_or_else(|| vec![])
.into_iter()
.chain(partial.from.unwrap_or_else(|| vec![]).into_iter())
.chain(partial.to.unwrap_or_else(|| vec![]).into_iter())
.map(PathBuf::from)
.map(StoreId::new)
.map(|r| r.map(Link::from))
@ -89,6 +110,48 @@ impl Linkable for Entry {
.map(LinkIter::new)
}
/// Get all links which are unidirectional links
fn unidirectional_links(&self) -> Result<LinkIter> {
trace!("Getting unidirectional links from header of '{}' = {:?}", self.get_location(), self.get_header());
let iter = self.get_header()
.read_partial::<LinkPartial>()?
.unwrap_or_else(Default::default)
.internal
.unwrap_or_else(|| vec![])
.into_iter();
link_string_iter_to_link_iter(iter)
}
/// Get all links which are directional links, outgoing
fn directional_links_to(&self) -> Result<LinkIter> {
trace!("Getting unidirectional (to) links from header of '{}' = {:?}", self.get_location(), self.get_header());
let iter = self.get_header()
.read_partial::<LinkPartial>()?
.unwrap_or_else(Default::default)
.to
.unwrap_or_else(|| vec![])
.into_iter();
link_string_iter_to_link_iter(iter)
}
/// Get all links which are directional links, incoming
fn directional_links_from(&self) -> Result<LinkIter> {
trace!("Getting unidirectional (from) links from header of '{}' = {:?}", self.get_location(), self.get_header());
let iter = self.get_header()
.read_partial::<LinkPartial>()?
.unwrap_or_else(Default::default)
.from
.unwrap_or_else(|| vec![])
.into_iter();
link_string_iter_to_link_iter(iter)
}
fn add_link(&mut self, other: &mut Entry) -> Result<()> {
debug!("Adding internal link: {:?}", other);
let left_location = self.get_location().to_str()?;
@ -150,6 +213,53 @@ impl Linkable for Entry {
Ok(())
}
fn add_link_to(&mut self, other: &mut Entry) -> Result<()> {
let left_location = self.get_location().to_str()?;
let right_location = other.get_location().to_str()?;
alter_linking(self, other, |mut left, mut right| {
let mut left_to = left.to.unwrap_or_else(|| vec![]);
left_to.push(right_location);
let mut right_from = right.from.unwrap_or_else(|| vec![]);
right_from.push(left_location);
left.to = Some(left_to);
right.from = Some(right_from);
Ok((left, right))
})
}
/// Remove a directional link: self -> otehr
fn remove_link_to(&mut self, other: &mut Entry) -> Result<()> {
let left_location = self.get_location().to_str()?;
let right_location = other.get_location().to_str()?;
alter_linking(self, other, |mut left, mut right| {
let mut left_to = left.to.unwrap_or_else(|| vec![]);
left_to.retain(|l| *l != right_location);
let mut right_from = right.from.unwrap_or_else(|| vec![]);
right_from.retain(|l| *l != left_location);
left.to = Some(left_to);
right.from = Some(right_from);
Ok((left, right))
})
}
}
fn link_string_iter_to_link_iter<I>(iter: I) -> Result<LinkIter>
where I: Iterator<Item = String>
{
iter.map(PathBuf::from)
.map(StoreId::new)
.map(|r| r.map(Link::from))
.collect::<Result<Vec<Link>>>()
.map(LinkIter::new)
}
fn alter_linking<F>(left: &mut Entry, right: &mut Entry, f: F) -> Result<()>
@ -392,4 +502,61 @@ mod test {
assert_eq!(e3.links().unwrap().collect::<Vec<_>>().len(), 0);
}
#[test]
fn test_directional_link() {
use libimagstore::store::Entry;
setup_logging();
let store = get_store();
let mut entry1 = store.create(PathBuf::from("test_directional_link-1")).unwrap();
let mut entry2 = store.create(PathBuf::from("test_directional_link-2")).unwrap();
assert!(entry1.unidirectional_links().unwrap().collect::<Vec<_>>().is_empty());
assert!(entry2.unidirectional_links().unwrap().collect::<Vec<_>>().is_empty());
assert!(entry1.directional_links_to().unwrap().collect::<Vec<_>>().is_empty());
assert!(entry2.directional_links_to().unwrap().collect::<Vec<_>>().is_empty());
assert!(entry1.directional_links_from().unwrap().collect::<Vec<_>>().is_empty());
assert!(entry2.directional_links_from().unwrap().collect::<Vec<_>>().is_empty());
assert!(entry1.add_link_to(&mut entry2).is_ok());
assert_eq!(entry1.unidirectional_links().unwrap().collect::<Vec<_>>(), vec![]);
assert_eq!(entry2.unidirectional_links().unwrap().collect::<Vec<_>>(), vec![]);
let get_directional_links_to = |e: &Entry| -> Result<Vec<String>, _> {
e.directional_links_to()
.unwrap()
.map(|l| l.to_str())
.collect::<Result<Vec<_>, _>>()
};
let get_directional_links_from = |e: &Entry| {
e.directional_links_from()
.unwrap()
.map(|l| l.to_str())
.collect::<Result<Vec<_>, _>>()
};
{
let entry1_dir_links = get_directional_links_to(&entry1).unwrap();
assert_eq!(entry1_dir_links, vec!["test_directional_link-2"]);
}
{
let entry2_dir_links = get_directional_links_to(&entry2).unwrap();
assert!(entry2_dir_links.is_empty());
}
{
let entry1_dir_links = get_directional_links_from(&entry1).unwrap();
assert!(entry1_dir_links.is_empty());
}
{
let entry2_dir_links = get_directional_links_from(&entry2).unwrap();
assert_eq!(entry2_dir_links, vec!["test_directional_link-1"]);
}
}
}