use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use crate::collection::{Collection, CollectionBuilder}; use crate::error::{DocTreeError, Result}; use crate::storage::RedisStorage; use crate::include::process_includes; use crate::utils::{name_fix, ensure_md_extension}; // Global variable to track the current collection name // This is for compatibility with the Go implementation lazy_static::lazy_static! { static ref CURRENT_COLLECTION_NAME: Arc>> = Arc::new(Mutex::new(None)); } // Global variable to track the current Collection // This is for compatibility with the Go implementation /// DocTree represents a manager for multiple collections pub struct DocTree { /// Map of collections by name pub collections: HashMap, /// Default collection name pub default_collection: Option, /// Redis storage backend storage: RedisStorage, /// For backward compatibility pub name: String, /// For backward compatibility pub path: PathBuf, } /// Builder for DocTree pub struct DocTreeBuilder { /// Map of collections by name collections: HashMap, /// Default collection name default_collection: Option, /// Redis storage backend storage: Option, /// For backward compatibility name: Option, /// For backward compatibility path: Option, } impl DocTree { /// Create a new DocTreeBuilder /// /// # Returns /// /// A new DocTreeBuilder pub fn builder() -> DocTreeBuilder { DocTreeBuilder { collections: HashMap::new(), default_collection: None, storage: None, name: None, path: None, } } /// Add a collection to the DocTree /// /// # Arguments /// /// * `path` - Base path of the collection /// * `name` - Name of the collection /// /// # Returns /// /// The added collection or an error pub fn add_collection>(&mut self, path: P, name: &str) -> Result<&Collection> { // Create a new collection let namefixed = name_fix(name); let collection = Collection::builder(path, &namefixed) .with_storage(self.storage.clone()) .build()?; // Scan the collection collection.scan()?; // Add to the collections map self.collections.insert(collection.name.clone(), collection); // Return a reference to the added collection self.collections.get(&namefixed).ok_or_else(|| { DocTreeError::CollectionNotFound(namefixed.clone()) }) } /// Get a collection by name /// /// # Arguments /// /// * `name` - Name of the collection /// /// # Returns /// /// The collection or an error pub fn get_collection(&self, name: &str) -> Result<&Collection> { // For compatibility with tests, apply namefix let namefixed = name_fix(name); // Check if the collection exists self.collections.get(&namefixed).ok_or_else(|| { DocTreeError::CollectionNotFound(name.to_string()) }) } /// Delete a collection from the DocTree /// /// # Arguments /// /// * `name` - Name of the collection /// /// # Returns /// /// Ok(()) on success or an error pub fn delete_collection(&mut self, name: &str) -> Result<()> { // For compatibility with tests, apply namefix let namefixed = name_fix(name); // Check if the collection exists if !self.collections.contains_key(&namefixed) { return Err(DocTreeError::CollectionNotFound(name.to_string())); } // Delete from Redis self.storage.delete_collection(&namefixed)?; // Remove from the collections map self.collections.remove(&namefixed); Ok(()) } /// List all collections /// /// # Returns /// /// A vector of collection names pub fn list_collections(&self) -> Vec { self.collections.keys().cloned().collect() } /// Get a page by name from a specific collection /// /// # Arguments /// /// * `collection_name` - Name of the collection (optional) /// * `page_name` - Name of the page /// /// # Returns /// /// The page content or an error pub fn page_get(&self, collection_name: Option<&str>, page_name: &str) -> Result { let (collection_name, page_name) = self.resolve_collection_and_page(collection_name, page_name)?; // Get the collection let collection = self.get_collection(&collection_name)?; // Get the page content let content = collection.page_get(page_name)?; // Process includes let processed_content = process_includes(&content, &collection_name, self)?; Ok(processed_content) } /// Get a page by name from a specific collection and return its HTML content /// /// # Arguments /// /// * `collection_name` - Name of the collection (optional) /// * `page_name` - Name of the page /// /// # Returns /// /// The HTML content or an error pub fn page_get_html(&self, collection_name: Option<&str>, page_name: &str) -> Result { let (collection_name, page_name) = self.resolve_collection_and_page(collection_name, page_name)?; // Get the collection let collection = self.get_collection(&collection_name)?; // Get the HTML collection.page_get_html(page_name, Some(self)) } /// Get the URL for a file in a specific collection /// /// # Arguments /// /// * `collection_name` - Name of the collection (optional) /// * `file_name` - Name of the file /// /// # Returns /// /// The URL for the file or an error pub fn file_get_url(&self, collection_name: Option<&str>, file_name: &str) -> Result { let (collection_name, file_name) = self.resolve_collection_and_page(collection_name, file_name)?; // Get the collection let collection = self.get_collection(&collection_name)?; // Get the URL collection.file_get_url(file_name) } /// Get the path to a page in the default collection /// /// # Arguments /// /// * `page_name` - Name of the page /// /// # Returns /// /// The path to the page or an error pub fn page_get_path(&self, page_name: &str) -> Result { // Check if a default collection is set let default_collection = self.default_collection.as_ref().ok_or_else(|| { DocTreeError::NoDefaultCollection })?; // Get the collection let collection = self.get_collection(default_collection)?; // Get the path collection.page_get_path(page_name) } /// Get information about the DocTree /// /// # Returns /// /// A map of information pub fn info(&self) -> HashMap { let mut info = HashMap::new(); info.insert("name".to_string(), self.name.clone()); info.insert("path".to_string(), self.path.to_string_lossy().to_string()); info.insert("collections".to_string(), self.collections.len().to_string()); info } /// Scan the default collection /// /// # Returns /// /// Ok(()) on success or an error pub fn scan(&self) -> Result<()> { // Check if a default collection is set let default_collection = self.default_collection.as_ref().ok_or_else(|| { DocTreeError::NoDefaultCollection })?; // Get the collection let collection = self.get_collection(default_collection)?; // Scan the collection collection.scan() } /// Resolve collection and page names /// /// # Arguments /// /// * `collection_name` - Name of the collection (optional) /// * `page_name` - Name of the page /// /// # Returns /// /// A tuple of (collection_name, page_name) or an error fn resolve_collection_and_page<'a>(&self, collection_name: Option<&'a str>, page_name: &'a str) -> Result<(String, &'a str)> { match collection_name { Some(name) => Ok((name_fix(name), page_name)), None => { // Use the default collection let default_collection = self.default_collection.as_ref().ok_or_else(|| { DocTreeError::NoDefaultCollection })?; Ok((default_collection.clone(), page_name)) } } } } impl DocTreeBuilder { /// Set the storage backend /// /// # Arguments /// /// * `storage` - Redis storage backend /// /// # Returns /// /// Self for method chaining pub fn with_storage(mut self, storage: RedisStorage) -> Self { self.storage = Some(storage); self } /// Add a collection /// /// # Arguments /// /// * `path` - Base path of the collection /// * `name` - Name of the collection /// /// # Returns /// /// Self for method chaining or an error pub fn with_collection>(mut self, path: P, name: &str) -> Result { // Ensure storage is set let storage = self.storage.as_ref().ok_or_else(|| { DocTreeError::MissingParameter("storage".to_string()) })?; // Create a new collection let namefixed = name_fix(name); let collection = Collection::builder(path.as_ref(), &namefixed) .with_storage(storage.clone()) .build()?; // Scan the collection collection.scan()?; // Add to the collections map self.collections.insert(collection.name.clone(), collection); // For backward compatibility if self.name.is_none() { self.name = Some(namefixed.clone()); } if self.path.is_none() { self.path = Some(path.as_ref().to_path_buf()); } Ok(self) } /// Set the default collection /// /// # Arguments /// /// * `name` - Name of the default collection /// /// # Returns /// /// Self for method chaining pub fn with_default_collection(mut self, name: &str) -> Self { self.default_collection = Some(name_fix(name)); self } /// Build the DocTree /// /// # Returns /// /// A new DocTree or an error pub fn build(self) -> Result { // Ensure storage is set let storage = self.storage.ok_or_else(|| { DocTreeError::MissingParameter("storage".to_string()) })?; // Create the DocTree let doctree = DocTree { collections: self.collections, default_collection: self.default_collection, storage: storage.clone(), name: self.name.unwrap_or_default(), path: self.path.unwrap_or_else(|| PathBuf::from("")), }; // Set the global current collection name if a default collection is set if let Some(default_collection) = &doctree.default_collection { let mut current_collection_name = CURRENT_COLLECTION_NAME.lock().unwrap(); *current_collection_name = Some(default_collection.clone()); } Ok(doctree) } } /// Create a new DocTree instance /// /// For backward compatibility, it also accepts path and name parameters /// to create a DocTree with a single collection /// /// # Arguments /// /// * `args` - Optional path and name for backward compatibility /// /// # Returns /// /// A new DocTree or an error pub fn new>(args: &[&str]) -> Result { let storage = RedisStorage::new("redis://localhost:6379")?; let mut builder = DocTree::builder().with_storage(storage); // For backward compatibility with existing code if args.len() == 2 { let path = args[0]; let name = args[1]; // Apply namefix for compatibility with tests let namefixed = name_fix(name); // Add the collection builder = builder.with_collection(path, &namefixed)?; // Set the default collection builder = builder.with_default_collection(&namefixed); } builder.build() }