doctree_rust/doctree/src/doctree.rs
2025-04-09 07:11:38 +02:00

433 lines
12 KiB
Rust

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<Mutex<Option<String>>> = 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<String, Collection>,
/// Default collection name
pub default_collection: Option<String>,
/// 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<String, Collection>,
/// Default collection name
default_collection: Option<String>,
/// Redis storage backend
storage: Option<RedisStorage>,
/// For backward compatibility
name: Option<String>,
/// For backward compatibility
path: Option<PathBuf>,
}
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<P: AsRef<Path>>(&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<String> {
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<String> {
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<String> {
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<String> {
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<String> {
// 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<String, String> {
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<P: AsRef<Path>>(mut self, path: P, name: &str) -> Result<Self> {
// 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<DocTree> {
// 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<P: AsRef<Path>>(args: &[&str]) -> Result<DocTree> {
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()
}