upend/src/database/hierarchies.rs

380 lines
11 KiB
Rust

use std::convert::TryFrom;
use anyhow::{anyhow, Result};
use diesel::sqlite::Sqlite;
use diesel::Connection;
use log::trace;
use serde_json::Value;
use uuid::Uuid;
use crate::addressing::Address;
use crate::database::constants::{
HIER_ADDR, HIER_HAS_ATTR, HIER_INVARIANT, IS_OF_TYPE_ATTR, LABEL_ATTR, TYPE_ADDR, TYPE_HAS_ATTR,
};
use crate::database::entry::{Entry, EntryValue};
use crate::database::lang::{EntryQuery, Query, QueryComponent, QueryPart};
use crate::database::{bulk_retrieve_objects, insert_entry, query, DbPool};
#[derive(Debug, Clone, PartialEq)]
pub struct UNode(String);
impl UNode {
pub fn new<T: Into<String>>(s: T) -> Result<Self> {
let s = s.into();
if s.is_empty() {
return Err(anyhow!("UNode can not be empty."));
}
Ok(Self(s))
}
pub fn as_ref(&self) -> &String {
&self.0
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct UPath(pub Vec<UNode>);
impl std::str::FromStr for UPath {
type Err = anyhow::Error;
fn from_str(string: &str) -> Result<Self, Self::Err> {
if string.is_empty() {
Ok(UPath(vec![]))
} else {
let result: Result<Vec<UNode>> = string
.trim_end_matches('/')
.split('/')
.map(|part| UNode::new(String::from(part)))
.collect();
Ok(UPath(result?))
}
}
}
impl std::fmt::Display for UNode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::fmt::Display for UPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
self.0
.iter()
.map(|node| node.to_string())
.collect::<Vec<String>>()
.join("/")
)
}
}
trait EntryList {
fn extract_addresses(&self) -> Vec<Address>;
}
impl EntryList for Vec<Entry> {
fn extract_addresses(&self) -> Vec<Address> {
self.iter()
.filter_map(|e| {
if let EntryValue::Address(address) = &e.value {
Some(address.clone())
} else {
None
}
})
.collect()
}
}
pub fn list_orphans<C: Connection<Backend = Sqlite>>(connection: &C) -> Result<Vec<Address>> {
let all_directories: Vec<Entry> = query(
connection,
Query::SingleQuery(QueryPart::Matches(EntryQuery {
entity: QueryComponent::Any,
attribute: QueryComponent::Exact(IS_OF_TYPE_ATTR.to_string()),
value: QueryComponent::Exact(EntryValue::Address(HIER_ADDR.clone())),
})),
)?;
let directories_with_parents: Vec<Address> = query(
connection,
Query::SingleQuery(QueryPart::Matches(EntryQuery {
entity: QueryComponent::Any,
attribute: QueryComponent::Exact(HIER_HAS_ATTR.to_string()),
value: QueryComponent::Any,
})),
)?
.extract_addresses();
Ok(all_directories
.into_iter()
.filter(|entry| !directories_with_parents.contains(&entry.entity))
.map(|e| e.entity)
.collect())
}
pub async fn list_node<C: Connection<Backend = Sqlite>>(
connection: &C,
path: &UPath,
) -> Result<Vec<Entry>> {
let resolved_path: Vec<Address> = resolve_path(connection, path, false)?;
let last = resolved_path.last().unwrap();
let entry_addresses = query(
connection,
Query::SingleQuery(QueryPart::Matches(EntryQuery {
entity: QueryComponent::Exact(last.clone()),
attribute: QueryComponent::Exact(HIER_HAS_ATTR.to_string()),
value: QueryComponent::Any,
})),
)?
.extract_addresses();
Ok(bulk_retrieve_objects(connection, entry_addresses)?
.into_iter()
// .filter(|e| [DIR_KEY, FILENAME_KEY, FILE_IDENTITY_KEY].contains(&e.attribute.as_str()))
.collect::<Vec<Entry>>())
}
pub fn fetch_or_create_dir<C: Connection<Backend = Sqlite>>(
connection: &C,
parent: Option<Address>,
directory: UNode,
create: bool,
) -> Result<Address> {
match parent.clone() {
Some(address) => trace!("FETCHING/CREATING {}/{:#}", address, directory),
None => trace!("FETCHING/CREATING /{:#}", directory),
}
let matching_directories = query(
connection,
Query::SingleQuery(QueryPart::Matches(EntryQuery {
entity: QueryComponent::Any,
attribute: QueryComponent::Exact(String::from(LABEL_ATTR)),
value: QueryComponent::Exact(EntryValue::Value(Value::String(
directory.as_ref().clone(),
))),
})),
)?
.into_iter()
.map(|e: Entry| e.entity);
let parent_has: Vec<Address> = match parent.clone() {
Some(parent) => query(
connection,
Query::SingleQuery(QueryPart::Matches(EntryQuery {
entity: QueryComponent::Exact(parent),
attribute: QueryComponent::Exact(String::from(HIER_HAS_ATTR)),
value: QueryComponent::Any,
})),
)?
.extract_addresses(),
None => list_orphans(connection)?,
};
let valid_directories: Vec<Address> = matching_directories
.filter(|a| parent_has.contains(a))
.collect();
match valid_directories.len() {
0 => {
if create {
let new_directory_address = Address::Uuid(Uuid::new_v4());
let type_entry = Entry {
entity: new_directory_address.clone(),
attribute: String::from(IS_OF_TYPE_ATTR),
value: EntryValue::Address(HIER_ADDR.clone()),
};
insert_entry(connection, type_entry)?;
let directory_entry = Entry {
entity: new_directory_address.clone(),
attribute: String::from(LABEL_ATTR),
value: EntryValue::Value(Value::String(directory.as_ref().clone())),
};
insert_entry(connection, directory_entry)?;
if let Some(parent) = parent {
let has_entry = Entry {
entity: parent,
attribute: String::from(HIER_HAS_ATTR),
value: EntryValue::Address(new_directory_address.clone()),
};
insert_entry(connection, has_entry)?;
}
Ok(new_directory_address)
} else {
Err(anyhow!("Directory does not exist."))
}
}
1 => Ok(valid_directories[0].clone()),
_ => Err(anyhow!(
"Invalid database state - more than one directory matches the query!"
)),
}
}
pub fn resolve_path<C: Connection<Backend = Sqlite>>(
connection: &C,
path: &UPath,
create: bool,
) -> Result<Vec<Address>> {
let mut result: Vec<Address> = vec![];
let mut path_stack = path.0.to_vec();
path_stack.reverse();
while !path_stack.is_empty() {
let dir_address = fetch_or_create_dir(
connection,
result.last().cloned(),
path_stack.pop().unwrap(),
create,
)?;
result.push(dir_address);
}
Ok(result)
}
pub fn initialize_hier(pool: &DbPool) -> Result<()> {
insert_entry(&pool.get()?, Entry::try_from(&*HIER_INVARIANT)?)?;
upend_insert_addr!(&pool.get()?, HIER_ADDR, IS_OF_TYPE_ATTR, TYPE_ADDR);
upend_insert_val!(&pool.get()?, HIER_ADDR, TYPE_HAS_ATTR, HIER_HAS_ATTR);
Ok(())
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use crate::database::open_upend;
use tempdir::TempDir;
use super::*;
#[test]
fn test_unode_nonempty() {
let node = UNode::new("foobar");
assert!(node.is_ok());
let node = UNode::new("");
assert!(node.is_err());
}
#[test]
fn test_path_codec() {
let path = UPath(vec![
UNode("top".to_string()),
UNode("foo".to_string()),
UNode("bar".to_string()),
UNode("baz".to_string()),
]);
let str_path = path.to_string();
assert!(!str_path.is_empty());
let decoded_path: Result<UPath> = str_path.parse();
assert!(decoded_path.is_ok());
assert_eq!(path, decoded_path.unwrap());
}
#[test]
fn test_path_validation() {
let valid_path: Result<UPath> = "a/b/c/d/e/f/g".parse();
assert!(valid_path.is_ok());
let invalid_path: Result<UPath> = "a/b/c//d/e/f/g".parse();
assert!(invalid_path.is_err());
let invalid_path: Result<UPath> = "a//b/c//d/e/f///g".parse();
assert!(invalid_path.is_err());
}
#[test]
fn test_path_manipulation() {
// Initialize database
let temp_dir = TempDir::new("upend-test").unwrap();
let open_result = open_upend(&temp_dir, None, true).unwrap();
let foo_result = fetch_or_create_dir(
&open_result.pool.get().unwrap(),
None,
UNode("foo".to_string()),
true,
);
assert!(foo_result.is_ok());
let bar_result = fetch_or_create_dir(
&open_result.pool.get().unwrap(),
None,
UNode("bar".to_string()),
true,
);
assert!(bar_result.is_ok());
let bar_result = bar_result.unwrap();
let baz_result = fetch_or_create_dir(
&open_result.pool.get().unwrap(),
Some(bar_result.clone()),
UNode("baz".to_string()),
true,
);
assert!(baz_result.is_ok());
let baz_result = baz_result.unwrap();
let orphans = list_orphans(&open_result.pool.get().unwrap());
assert!(orphans.is_ok());
assert_eq!(orphans.unwrap().len(), 2);
let resolve_result = resolve_path(
&open_result.pool.get().unwrap(),
&"bar/baz".parse().unwrap(),
false,
);
assert!(resolve_result.is_ok());
assert_eq!(
resolve_result.unwrap(),
vec![bar_result.clone(), baz_result.clone()]
);
let resolve_result = resolve_path(
&open_result.pool.get().unwrap(),
&"bar/baz/bax".parse().unwrap(),
false,
);
assert!(resolve_result.is_err());
let resolve_result = resolve_path(
&open_result.pool.get().unwrap(),
&"bar/baz/bax".parse().unwrap(),
true,
);
assert!(resolve_result.is_ok());
let bax_result = fetch_or_create_dir(
&open_result.pool.get().unwrap(),
Some(baz_result.clone()),
UNode("bax".to_string()),
false,
);
assert!(bax_result.is_ok());
let bax_result = bax_result.unwrap();
assert_eq!(
resolve_result.unwrap(),
vec![bar_result, baz_result, bax_result]
);
}
}