use crate::collection::Document;
use crate::common::{get_key_name, get_keyed_repo_type, Key, Value, TAG_COLLECTION, TAG_KEYED_REPOSITORIES, TAG_REPOSITORIES};
use crate::errors::{ErrorKind, NitriteError, NitriteResult};
use crate::store::{MapMeta, Metadata, NitriteMap, NitriteMapProvider};
use std::collections::{HashMap, HashSet};
use std::ops::Deref;
use std::sync::Arc;
#[derive(Clone)]
pub struct StoreCatalog {
inner: Arc<StoreCatalogInner>,
}
impl StoreCatalog {
pub fn new(catalog_map: NitriteMap) -> NitriteResult<StoreCatalog> {
Ok(StoreCatalog {
inner: Arc::new(StoreCatalogInner { catalog_map }),
})
}
}
impl Deref for StoreCatalog {
type Target = StoreCatalogInner;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
pub struct StoreCatalogInner {
catalog_map: NitriteMap,
}
impl StoreCatalogInner {
pub fn has_entry(&self, name: &str) -> NitriteResult<bool> {
let catalog_map = self.get_catalog_map()?;
for entry in catalog_map.entries()? {
match entry {
Ok(pair) => {
match pair.1.as_document() {
Some(document) => {
let meta_data = MapMeta::new(document);
if meta_data.map_names.contains(name) {
return Ok(true);
}
}
None => {
log::warn!("StoreCatalog: Skipping invalid catalog entry format (expected Document, got non-Document type)");
continue;
}
}
}
Err(e) => {
return Err(e);
}
}
}
Ok(false)
}
pub fn write_collection_entry(&self, name: &str) -> NitriteResult<()> {
if name.is_empty() {
log::error!("Collection name cannot be empty");
return Err(NitriteError::new(
"Collection name cannot be empty",
ErrorKind::ValidationError,
));
}
let catalog_map = self.get_catalog_map()?;
let document = catalog_map.get(&Value::from(TAG_COLLECTION))?;
let document = match document {
None => Document::new(),
Some(doc) => {
let doc_ref = doc.as_document()
.ok_or_else(|| NitriteError::new(
"StoreCatalog: Invalid collection entry format (expected Document)",
ErrorKind::InvalidOperation
))?;
doc_ref.clone()
}
};
let mut meta_data = MapMeta::new(&document);
meta_data.map_names.insert(name.to_string());
catalog_map.put(
Key::from(TAG_COLLECTION.to_string()),
Value::from(meta_data.get_info()),
)?;
Ok(())
}
pub fn write_repository_entry(&self, name: &str) -> NitriteResult<()> {
if name.is_empty() {
log::error!("Repository name cannot be empty");
return Err(NitriteError::new(
"Repository name cannot be empty",
ErrorKind::ValidationError,
));
}
let catalog_map = self.get_catalog_map()?;
let document = catalog_map.get(&Value::from(TAG_REPOSITORIES))?;
let document = match document {
None => Document::new(),
Some(doc) => {
let doc_ref = doc.as_document()
.ok_or_else(|| NitriteError::new(
"StoreCatalog: Invalid repository entry format (expected Document)",
ErrorKind::InvalidOperation
))?;
doc_ref.clone()
}
};
let mut meta_data = MapMeta::new(&document);
meta_data.map_names.insert(name.to_string());
catalog_map.put(
Key::from(TAG_REPOSITORIES.to_string()),
Value::from(meta_data.get_info()),
)?;
Ok(())
}
pub fn write_keyed_repository_entry(&self, name: &str) -> NitriteResult<()> {
if name.is_empty() {
log::error!("Keyed repository name cannot be empty");
return Err(NitriteError::new(
"Keyed repository name cannot be empty",
ErrorKind::ValidationError,
));
}
let catalog_map = self.get_catalog_map()?;
let document = catalog_map.get(&Value::from(TAG_KEYED_REPOSITORIES))?;
let document = match document {
None => Document::new(),
Some(doc) => {
let doc_ref = doc.as_document()
.ok_or_else(|| NitriteError::new(
"StoreCatalog: Invalid keyed repository entry format (expected Document)",
ErrorKind::InvalidOperation
))?;
doc_ref.clone()
}
};
let mut meta_data = MapMeta::new(&document);
meta_data.map_names.insert(name.to_string());
catalog_map.put(
Key::from(TAG_KEYED_REPOSITORIES.to_string()),
Value::from(meta_data.get_info()),
)?;
Ok(())
}
pub fn get_collection_names(&self) -> NitriteResult<HashSet<String>> {
let catalog_map = self.get_catalog_map()?;
let document = catalog_map.get(&Value::from(TAG_COLLECTION))?;
let document = match document {
None => Document::new(),
Some(doc) => {
let doc_ref = doc.as_document()
.ok_or_else(|| NitriteError::new(
"StoreCatalog: Invalid collection entry format (expected Document)",
ErrorKind::InvalidOperation
))?;
doc_ref.clone()
}
};
let meta_data = MapMeta::new(&document);
Ok(meta_data.map_names)
}
pub fn get_repository_names(&self) -> NitriteResult<HashSet<String>> {
let catalog_map = self.get_catalog_map()?;
let document = catalog_map.get(&Value::from(TAG_REPOSITORIES))?;
let document = match document {
None => Document::new(),
Some(doc) => {
let doc_ref = doc.as_document()
.ok_or_else(|| NitriteError::new(
"StoreCatalog: Invalid repository entry format (expected Document)",
ErrorKind::InvalidOperation
))?;
doc_ref.clone()
}
};
let meta_data = MapMeta::new(&document);
Ok(meta_data.map_names)
}
pub fn get_keyed_repository_names(&self) -> NitriteResult<HashMap<String, HashSet<String>>> {
let catalog_map = self.get_catalog_map()?;
let document = catalog_map.get(&Value::from(TAG_KEYED_REPOSITORIES))?;
let document = match document {
None => Document::new(),
Some(doc) => {
let doc_ref = doc.as_document()
.ok_or_else(|| NitriteError::new(
"StoreCatalog: Invalid keyed repository entry format (expected Document)",
ErrorKind::InvalidOperation
))?;
doc_ref.clone()
}
};
let meta_data = MapMeta::new(&document);
let keyed_repository_names = meta_data.map_names;
let mut result: HashMap<String, HashSet<String>> = HashMap::with_capacity(keyed_repository_names.len());
for name in keyed_repository_names {
let key = get_key_name(&name)?;
let repo_type = get_keyed_repo_type(&name)?;
let types = result.entry(key).or_default();
types.insert(repo_type);
}
Ok(result)
}
pub fn remove(&self, name: &str) -> NitriteResult<()> {
let mut updated_map = HashMap::new();
let catalog_map = self.get_catalog_map()?;
let entries = catalog_map.entries()?;
for entry in entries {
match entry {
Ok(pair) => {
let catalog_name = pair.0;
let document = pair.1.as_document()
.ok_or_else(|| NitriteError::new(
"StoreCatalog: Invalid catalog entry format (expected Document)",
ErrorKind::InvalidOperation
))?;
let mut meta_data = MapMeta::new(document);
if meta_data.map_names.contains(name) {
meta_data.map_names.remove(name);
updated_map.insert(catalog_name.clone(), meta_data.get_info());
break;
}
}
Err(e) => {
return Err(e);
}
}
}
for (catalog_name, meta_data) in updated_map {
catalog_map.put(catalog_name, Value::from(meta_data))?;
}
Ok(())
}
fn get_catalog_map(&self) -> NitriteResult<NitriteMap> {
Ok(self.catalog_map.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::common::KEY_OBJ_SEPARATOR;
use crate::store::memory::{InMemoryMap, InMemoryStore, InMemoryStoreConfig};
use crate::store::NitriteStore;
fn setup_catalog() -> StoreCatalog {
let store = InMemoryStore::new(InMemoryStoreConfig::new());
let catalog_map = InMemoryMap::new("test", NitriteStore::new(store));
StoreCatalog::new(NitriteMap::new(catalog_map)).unwrap()
}
#[test]
fn test_has_entry() {
let catalog = setup_catalog();
assert!(!catalog.has_entry("test_entry").unwrap());
catalog.write_collection_entry("test_entry").unwrap();
assert!(catalog.has_entry("test_entry").unwrap());
}
#[test]
fn test_write_collection_entry() {
let catalog = setup_catalog();
catalog.write_collection_entry("test_entry").unwrap();
assert!(catalog.has_entry("test_entry").unwrap());
}
#[test]
fn test_write_repository_entry() {
let catalog = setup_catalog();
catalog.write_repository_entry("test_repo").unwrap();
assert!(catalog.has_entry("test_repo").unwrap());
}
#[test]
fn test_write_keyed_repository_entry() {
let catalog = setup_catalog();
catalog.write_keyed_repository_entry("test_keyed_repo").unwrap();
assert!(catalog.has_entry("test_keyed_repo").unwrap());
}
#[test]
fn test_get_collection_names() {
let catalog = setup_catalog();
catalog.write_collection_entry("test_entry").unwrap();
let names = catalog.get_collection_names().unwrap();
assert!(names.contains("test_entry"));
}
#[test]
fn test_get_repository_names() {
let catalog = setup_catalog();
catalog.write_repository_entry("test_repo").unwrap();
let names = catalog.get_repository_names().unwrap();
assert!(names.contains("test_repo"));
}
#[test]
fn test_get_keyed_repository_names() {
let catalog = setup_catalog();
let keyed_repo_name = format!("{}{}{}", "test_keyed_repo", KEY_OBJ_SEPARATOR, "test_key");
catalog.write_keyed_repository_entry(&keyed_repo_name).unwrap();
let names = catalog.get_keyed_repository_names().unwrap();
assert!(names.contains_key("test_key"));
assert!(names.get("test_key").unwrap().contains("test_keyed_repo"));
}
#[test]
fn test_remove() {
let catalog = setup_catalog();
catalog.write_collection_entry("test_entry").unwrap();
assert!(catalog.has_entry("test_entry").unwrap());
catalog.remove("test_entry").unwrap();
assert!(!catalog.has_entry("test_entry").unwrap());
}
#[test]
fn test_has_entry_negative() {
let catalog = setup_catalog();
assert!(!catalog.has_entry("non_existent_entry").unwrap());
}
#[test]
fn test_write_collection_entry_negative() {
let catalog = setup_catalog();
assert!(catalog.write_collection_entry("").is_err());
}
#[test]
fn test_write_repository_entry_negative() {
let catalog = setup_catalog();
assert!(catalog.write_repository_entry("").is_err());
}
#[test]
fn test_write_keyed_repository_entry_negative() {
let catalog = setup_catalog();
assert!(catalog.write_keyed_repository_entry("").is_err());
}
#[test]
fn test_get_collection_names_empty() {
let catalog = setup_catalog();
let names = catalog.get_collection_names().unwrap();
assert!(names.is_empty());
}
#[test]
fn test_get_repository_names_empty() {
let catalog = setup_catalog();
let names = catalog.get_repository_names().unwrap();
assert!(names.is_empty());
}
#[test]
fn test_get_keyed_repository_names_empty() {
let catalog = setup_catalog();
let names = catalog.get_keyed_repository_names().unwrap();
assert!(names.is_empty());
}
#[test]
fn test_remove_non_existent_entry() {
let catalog = setup_catalog();
assert!(catalog.remove("non_existent_entry").is_ok());
}
#[test]
fn test_has_entry_handles_corrupted_document() {
let catalog = setup_catalog();
catalog.write_collection_entry("test_entry").unwrap();
let catalog_map = catalog.get_catalog_map().unwrap();
catalog_map.put(
Key::from(TAG_COLLECTION.to_string()),
Value::from("corrupted_string"),
).unwrap();
let result = catalog.has_entry("test_entry");
assert!(result.is_ok(), "Should handle corrupted entries gracefully");
assert!(!result.unwrap(), "Should return false since the entry is not found after skipping corruption");
}
#[test]
fn test_write_collection_entry_handles_corrupted_data() {
let catalog = setup_catalog();
let catalog_map = catalog.get_catalog_map().unwrap();
catalog_map.put(
Key::from(TAG_COLLECTION.to_string()),
Value::from(42i32), ).unwrap();
let result = catalog.write_collection_entry("new_entry");
assert!(result.is_err(), "Should return error for corrupted document");
if let Err(e) = result {
assert!(e.to_string().contains("Invalid collection entry format"));
}
}
#[test]
fn test_write_repository_entry_handles_corrupted_data() {
let catalog = setup_catalog();
let catalog_map = catalog.get_catalog_map().unwrap();
catalog_map.put(
Key::from(TAG_REPOSITORIES.to_string()),
Value::from("corrupted"),
).unwrap();
let result = catalog.write_repository_entry("new_repo");
assert!(result.is_err(), "Should return error for corrupted document");
if let Err(e) = result {
assert!(e.to_string().contains("Invalid repository entry format"));
}
}
#[test]
fn test_write_keyed_repository_entry_handles_corrupted_data() {
let catalog = setup_catalog();
let catalog_map = catalog.get_catalog_map().unwrap();
catalog_map.put(
Key::from(TAG_KEYED_REPOSITORIES.to_string()),
Value::from("corrupted_keyed"),
).unwrap();
let result = catalog.write_keyed_repository_entry("new_keyed_repo");
assert!(result.is_err(), "Should return error for corrupted document");
if let Err(e) = result {
assert!(e.to_string().contains("Invalid keyed repository entry format"));
}
}
#[test]
fn test_get_collection_names_handles_corrupted_data() {
let catalog = setup_catalog();
let catalog_map = catalog.get_catalog_map().unwrap();
catalog_map.put(
Key::from(TAG_COLLECTION.to_string()),
Value::from(3.14f64),
).unwrap();
let result = catalog.get_collection_names();
assert!(result.is_err(), "Should return error for corrupted document");
}
#[test]
fn test_get_repository_names_handles_corrupted_data() {
let catalog = setup_catalog();
let catalog_map = catalog.get_catalog_map().unwrap();
catalog_map.put(
Key::from(TAG_REPOSITORIES.to_string()),
Value::from(vec![1, 2, 3]),
).unwrap();
let result = catalog.get_repository_names();
assert!(result.is_err(), "Should return error for corrupted document");
}
#[test]
fn test_get_keyed_repository_names_handles_corrupted_data() {
let catalog = setup_catalog();
let catalog_map = catalog.get_catalog_map().unwrap();
catalog_map.put(
Key::from(TAG_KEYED_REPOSITORIES.to_string()),
Value::from(true),
).unwrap();
let result = catalog.get_keyed_repository_names();
assert!(result.is_err(), "Should return error for corrupted document");
}
#[test]
fn test_remove_handles_corrupted_document() {
let catalog = setup_catalog();
catalog.write_collection_entry("test_entry").unwrap();
let catalog_map = catalog.get_catalog_map().unwrap();
catalog_map.put(
Key::from(TAG_COLLECTION.to_string()),
Value::from("corrupted"),
).unwrap();
let result = catalog.remove("test_entry");
assert!(result.is_err(), "Should return error for corrupted document");
}
#[test]
fn test_multiple_catalog_operations_all_safe() {
let catalog = setup_catalog();
catalog.write_collection_entry("col1").unwrap();
catalog.write_repository_entry("repo1").unwrap();
let cols = catalog.get_collection_names().unwrap();
assert!(cols.contains("col1"));
let repos = catalog.get_repository_names().unwrap();
assert!(repos.contains("repo1"));
let catalog_map = catalog.get_catalog_map().unwrap();
catalog_map.put(
Key::from(TAG_COLLECTION.to_string()),
Value::from("bad_data"),
).unwrap();
assert!(catalog.get_collection_names().is_err());
assert!(catalog.write_collection_entry("col2").is_err());
let has_col1 = catalog.has_entry("col1");
assert!(has_col1.is_ok() && !has_col1.unwrap(), "Should gracefully skip corrupted entries");
assert!(catalog.has_entry("repo1").is_ok());
}
#[test]
fn test_get_keyed_repository_names_with_pre_allocation() {
let catalog = setup_catalog();
let keyed_repo_name1 = format!("{}{}{}", "repo1", KEY_OBJ_SEPARATOR, "key1");
let keyed_repo_name2 = format!("{}{}{}", "repo2", KEY_OBJ_SEPARATOR, "key1");
let keyed_repo_name3 = format!("{}{}{}", "repo1", KEY_OBJ_SEPARATOR, "key2");
catalog.write_keyed_repository_entry(&keyed_repo_name1).unwrap();
catalog.write_keyed_repository_entry(&keyed_repo_name2).unwrap();
catalog.write_keyed_repository_entry(&keyed_repo_name3).unwrap();
let names = catalog.get_keyed_repository_names().unwrap();
assert_eq!(names.len(), 2);
assert!(names.contains_key("key1"));
assert!(names.contains_key("key2"));
assert_eq!(names.get("key1").unwrap().len(), 2);
assert!(names.get("key1").unwrap().contains("repo1"));
assert!(names.get("key1").unwrap().contains("repo2"));
assert_eq!(names.get("key2").unwrap().len(), 1);
assert!(names.get("key2").unwrap().contains("repo1"));
}
#[test]
fn test_get_keyed_repository_names_entry_api_efficiency() {
let catalog = setup_catalog();
for i in 0..5 {
let keyed_repo_name = format!("repo_eff{}{}{}", i, KEY_OBJ_SEPARATOR, "key_same");
catalog.write_keyed_repository_entry(&keyed_repo_name).unwrap();
}
let names = catalog.get_keyed_repository_names().unwrap();
assert_eq!(names.len(), 1);
let repo_types = names.get("key_same").unwrap();
assert_eq!(repo_types.len(), 5);
}
#[test]
fn test_large_keyed_repository_set_efficiency() {
let catalog = setup_catalog();
for i in 0..50 {
let repo_num = i / 5;
let key_num = i % 5;
let keyed_repo_name = format!("repo{}{}key{}", repo_num, KEY_OBJ_SEPARATOR, key_num);
catalog.write_keyed_repository_entry(&keyed_repo_name).unwrap();
}
let names = catalog.get_keyed_repository_names().unwrap();
assert_eq!(names.len(), 5);
for i in 0..5 {
let key = format!("key{}", i);
assert_eq!(names.get(&key).unwrap().len(), 10);
}
}
#[test]
fn test_entry_api_avoids_clones() {
let catalog = setup_catalog();
let keyed_repo_name = format!("{}{}{}", "repo_clone_test", KEY_OBJ_SEPARATOR, "key_clone");
catalog.write_keyed_repository_entry(&keyed_repo_name).unwrap();
let names = catalog.get_keyed_repository_names().unwrap();
assert!(names.contains_key("key_clone"));
assert_eq!(names.get("key_clone").unwrap().len(), 1);
}
}