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>(s: T) -> Result { 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); impl std::str::FromStr for UPath { type Err = anyhow::Error; fn from_str(string: &str) -> Result { if string.is_empty() { Ok(UPath(vec![])) } else { let result: Result> = 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::>() .join("/") ) } } trait EntryList { fn extract_addresses(&self) -> Vec
; } impl EntryList for Vec { fn extract_addresses(&self) -> Vec
{ self.iter() .filter_map(|e| { if let EntryValue::Address(address) = &e.value { Some(address.clone()) } else { None } }) .collect() } } pub fn list_orphans>(connection: &C) -> Result> { let all_directories: Vec = 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
= 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>( connection: &C, path: &UPath, ) -> Result> { let resolved_path: Vec
= 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::>()) } pub fn fetch_or_create_dir>( connection: &C, parent: Option
, directory: UNode, create: bool, ) -> Result
{ 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
= 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
= 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 parent.is_some() { let has_entry = Entry { entity: parent.unwrap(), 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>( connection: &C, path: &UPath, create: bool, ) -> Result> { let mut result: Vec
= 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 super::{UNode, UPath}; #[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 = str_path.parse(); assert!(decoded_path.is_ok()); assert_eq!(path, decoded_path.unwrap()); } #[test] fn test_validation() { let valid_path: Result = "a/b/c/d/e/f/g".parse(); assert!(valid_path.is_ok()); let invalid_path: Result = "a/b/c//d/e/f/g".parse(); assert!(invalid_path.is_err()); let invalid_path: Result = "a//b/c//d/e/f///g".parse(); assert!(invalid_path.is_err()); } }