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:
parent
56f8aecdbf
commit
0eac6d9931
2 changed files with 193 additions and 5 deletions
|
@ -30,6 +30,8 @@ use failure::Error;
|
||||||
#[derive(Eq, PartialOrd, Ord, Hash, Debug, Clone)]
|
#[derive(Eq, PartialOrd, Ord, Hash, Debug, Clone)]
|
||||||
pub enum Link {
|
pub enum Link {
|
||||||
Id { link: StoreId },
|
Id { link: StoreId },
|
||||||
|
LinkTo { link: StoreId },
|
||||||
|
LinkFrom { link: StoreId },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Link {
|
impl Link {
|
||||||
|
@ -37,6 +39,8 @@ impl Link {
|
||||||
pub fn exists(&self, store: &Store) -> Result<bool> {
|
pub fn exists(&self, store: &Store) -> Result<bool> {
|
||||||
match *self {
|
match *self {
|
||||||
Link::Id { ref link } => store.exists(link.clone()),
|
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)
|
.map_err(From::from)
|
||||||
}
|
}
|
||||||
|
@ -44,6 +48,8 @@ impl Link {
|
||||||
pub fn to_str(&self) -> Result<String> {
|
pub fn to_str(&self) -> Result<String> {
|
||||||
match *self {
|
match *self {
|
||||||
Link::Id { ref link } => link.to_str(),
|
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)
|
.map_err(From::from)
|
||||||
}
|
}
|
||||||
|
@ -52,6 +58,8 @@ impl Link {
|
||||||
pub(crate) fn eq_store_id(&self, id: &StoreId) -> bool {
|
pub(crate) fn eq_store_id(&self, id: &StoreId) -> bool {
|
||||||
match self {
|
match self {
|
||||||
&Link::Id { link: ref s } => s.eq(id),
|
&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 {
|
pub fn get_store_id(&self) -> &StoreId {
|
||||||
match self {
|
match self {
|
||||||
&Link::Id { link: ref s } => s,
|
&Link::Id { link: ref s } => s,
|
||||||
|
&Link::LinkTo { ref link } => link,
|
||||||
|
&Link::LinkFrom { ref link } => link,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn to_value(&self) -> Result<Value> {
|
pub(crate) fn to_value(&self) -> Result<Value> {
|
||||||
match self {
|
match self {
|
||||||
Link::Id { ref link } => link
|
Link::Id { ref link } => link,
|
||||||
.to_str()
|
Link::LinkTo { ref link } => link,
|
||||||
.map(Value::String)
|
Link::LinkFrom { ref link } => link,
|
||||||
.context(EM::ConversionError)
|
|
||||||
.map_err(Error::from),
|
|
||||||
}
|
}
|
||||||
|
.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 {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
match (self, other) {
|
match (self, other) {
|
||||||
(&Link::Id { link: ref a }, &Link::Id { link: ref b }) => a.eq(&b),
|
(&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 {
|
fn into(self) -> StoreId {
|
||||||
match self {
|
match self {
|
||||||
Link::Id { link } => link,
|
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> {
|
fn into_storeid(self) -> Result<StoreId> {
|
||||||
match self {
|
match self {
|
||||||
Link::Id { link } => Ok(link),
|
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 {
|
fn as_ref(&self) -> &StoreId {
|
||||||
match self {
|
match self {
|
||||||
&Link::Id { ref link } => &link,
|
&Link::Id { ref link } => &link,
|
||||||
|
&Link::LinkTo { ref link } => &link,
|
||||||
|
&Link::LinkFrom { ref link } => &link,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,15 @@ pub trait Linkable {
|
||||||
/// Get all links
|
/// Get all links
|
||||||
fn links(&self) -> Result<LinkIter>;
|
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
|
/// Add an internal link to the implementor object
|
||||||
fn add_link(&mut self, link: &mut Entry) -> Result<()>;
|
fn add_link(&mut self, link: &mut Entry) -> Result<()>;
|
||||||
|
|
||||||
|
@ -47,17 +56,27 @@ pub trait Linkable {
|
||||||
/// Remove _all_ internal links
|
/// Remove _all_ internal links
|
||||||
fn unlink(&mut self, store: &Store) -> Result<()>;
|
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)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
struct LinkPartial {
|
struct LinkPartial {
|
||||||
internal: Option<Vec<String>>,
|
internal: Option<Vec<String>>,
|
||||||
|
from: Option<Vec<String>>,
|
||||||
|
to: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for LinkPartial {
|
impl Default for LinkPartial {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
LinkPartial {
|
LinkPartial {
|
||||||
internal: None,
|
internal: None,
|
||||||
|
from: None,
|
||||||
|
to: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,6 +101,8 @@ impl Linkable for Entry {
|
||||||
.internal
|
.internal
|
||||||
.unwrap_or_else(|| vec![])
|
.unwrap_or_else(|| vec![])
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
.chain(partial.from.unwrap_or_else(|| vec![]).into_iter())
|
||||||
|
.chain(partial.to.unwrap_or_else(|| vec![]).into_iter())
|
||||||
.map(PathBuf::from)
|
.map(PathBuf::from)
|
||||||
.map(StoreId::new)
|
.map(StoreId::new)
|
||||||
.map(|r| r.map(Link::from))
|
.map(|r| r.map(Link::from))
|
||||||
|
@ -89,6 +110,48 @@ impl Linkable for Entry {
|
||||||
.map(LinkIter::new)
|
.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<()> {
|
fn add_link(&mut self, other: &mut Entry) -> Result<()> {
|
||||||
debug!("Adding internal link: {:?}", other);
|
debug!("Adding internal link: {:?}", other);
|
||||||
let left_location = self.get_location().to_str()?;
|
let left_location = self.get_location().to_str()?;
|
||||||
|
@ -150,6 +213,53 @@ impl Linkable for Entry {
|
||||||
Ok(())
|
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<()>
|
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);
|
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"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue