14 KiB
14 KiB
DocTree Implementation Plan
Overview
The DocTree library will be a Rust implementation of the Go reference, maintaining the core functionality while improving the API design to be more idiomatic Rust. We'll use Redis as the storage backend and implement a minimal CLI example to demonstrate usage.
Architecture
classDiagram
class DocTree {
+collections: HashMap<String, Collection>
+default_collection: Option<String>
+new() DocTreeBuilder
+add_collection(path, name) Result<&Collection>
+get_collection(name) Result<&Collection>
+delete_collection(name) Result<()>
+list_collections() Vec<String>
+page_get(collection, page) Result<String>
+page_get_html(collection, page) Result<String>
+file_get_url(collection, file) Result<String>
}
class DocTreeBuilder {
-collections: HashMap<String, Collection>
-default_collection: Option<String>
+with_collection(path, name) DocTreeBuilder
+with_default_collection(name) DocTreeBuilder
+build() Result<DocTree>
}
class Collection {
+path: String
+name: String
+new(path, name) CollectionBuilder
+scan() Result<()>
+page_get(name) Result<String>
+page_set(name, content) Result<()>
+page_delete(name) Result<()>
+page_list() Result<Vec<String>>
+file_get_url(name) Result<String>
+file_set(name, content) Result<()>
+file_delete(name) Result<()>
+file_list() Result<Vec<String>>
+page_get_html(name) Result<String>
}
class CollectionBuilder {
-path: String
-name: String
+build() Result<Collection>
}
class RedisStorage {
+client: redis::Client
+new(url) Result<RedisStorage>
+store_collection_entry(collection, key, value) Result<()>
+get_collection_entry(collection, key) Result<String>
+delete_collection_entry(collection, key) Result<()>
+list_collection_entries(collection) Result<Vec<String>>
+delete_collection(collection) Result<()>
}
class IncludeProcessor {
+process_includes(content, collection, doctree) Result<String>
}
DocTree --> DocTreeBuilder : creates
DocTree --> "0..*" Collection : contains
Collection --> CollectionBuilder : creates
DocTree --> RedisStorage : uses
Collection --> RedisStorage : uses
DocTree --> IncludeProcessor : uses
Implementation Steps
1. Project Setup and Dependencies
- Update the Cargo.toml files with necessary dependencies:
- redis (for Redis client)
- walkdir (for directory traversal)
- pulldown-cmark (for Markdown to HTML conversion)
- thiserror (for error handling)
- clap (for CLI argument parsing in doctreecmd)
2. Core Library Structure
-
Error Module
- Create a custom error type using thiserror
- Define specific error variants for different failure scenarios
-
Storage Module
- Implement the RedisStorage struct to handle Redis operations
- Provide methods for storing, retrieving, and deleting collection entries
- Implement connection pooling for efficient Redis access
-
Utils Module
- Implement utility functions like name_fix (equivalent to tools.NameFix in Go)
- Implement markdown to HTML conversion using pulldown-cmark
3. Collection Implementation
-
Collection Module
- Implement the Collection struct to represent a collection of documents
- Implement the CollectionBuilder for creating Collection instances
- Implement methods for scanning directories, managing pages and files
-
Collection Builder Pattern
- Create a builder pattern for Collection creation
- Allow configuration of Collection properties before building
4. DocTree Implementation
-
DocTree Module
- Implement the DocTree struct to manage multiple collections
- Implement the DocTreeBuilder for creating DocTree instances
- Implement methods for managing collections and accessing documents
-
DocTree Builder Pattern
- Create a builder pattern for DocTree creation
- Allow adding collections and setting default collection before building
5. Include Processor Implementation
- Include Module
- Implement the IncludeProcessor to handle include directives
- Implement parsing of include directives
- Implement recursive processing of includes
6. CLI Example
- Update doctreecmd
- Implement a minimal CLI interface using clap
- Provide commands for basic operations:
- Scanning a directory
- Listing collections
- Getting page content
- Getting HTML content
Detailed Module Breakdown
Error Module (src/error.rs)
use thiserror::Error;
#[derive(Error, Debug)]
pub enum DocTreeError {
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("Redis error: {0}")]
RedisError(#[from] redis::RedisError),
#[error("Collection not found: {0}")]
CollectionNotFound(String),
#[error("Page not found: {0}")]
PageNotFound(String),
#[error("File not found: {0}")]
FileNotFound(String),
#[error("Invalid include directive: {0}")]
InvalidIncludeDirective(String),
#[error("No default collection set")]
NoDefaultCollection,
#[error("Invalid number of arguments")]
InvalidArgumentCount,
}
pub type Result<T> = std::result::Result<T, DocTreeError>;
Storage Module (src/storage.rs)
use redis::{Client, Commands, Connection};
use crate::error::{DocTreeError, Result};
pub struct RedisStorage {
client: Client,
}
impl RedisStorage {
pub fn new(url: &str) -> Result<Self> {
let client = Client::open(url)?;
Ok(Self { client })
}
pub fn get_connection(&self) -> Result<Connection> {
Ok(self.client.get_connection()?)
}
pub fn store_collection_entry(&self, collection: &str, key: &str, value: &str) -> Result<()> {
let mut conn = self.get_connection()?;
let collection_key = format!("collections:{}", collection);
conn.hset(collection_key, key, value)?;
Ok(())
}
pub fn get_collection_entry(&self, collection: &str, key: &str) -> Result<String> {
let mut conn = self.get_connection()?;
let collection_key = format!("collections:{}", collection);
let value: String = conn.hget(collection_key, key)?;
Ok(value)
}
// Additional methods for Redis operations
}
Utils Module (src/utils.rs)
use pulldown_cmark::{Parser, Options, html};
pub fn name_fix(name: &str) -> String {
// Implementation of name_fix similar to tools.NameFix in Go
// Normalize the name by converting to lowercase, replacing spaces with hyphens, etc.
}
pub fn markdown_to_html(markdown: &str) -> String {
let mut options = Options::empty();
options.insert(Options::ENABLE_TABLES);
options.insert(Options::ENABLE_FOOTNOTES);
options.insert(Options::ENABLE_STRIKETHROUGH);
let parser = Parser::new_ext(markdown, options);
let mut html_output = String::new();
html::push_html(&mut html_output, parser);
html_output
}
Collection Module (src/collection.rs)
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
use crate::error::Result;
use crate::storage::RedisStorage;
use crate::utils::name_fix;
pub struct Collection {
pub path: PathBuf,
pub name: String,
storage: RedisStorage,
}
pub struct CollectionBuilder {
path: PathBuf,
name: String,
storage: Option<RedisStorage>,
}
impl Collection {
pub fn builder<P: AsRef<Path>>(path: P, name: &str) -> CollectionBuilder {
CollectionBuilder {
path: path.as_ref().to_path_buf(),
name: name_fix(name),
storage: None,
}
}
pub fn scan(&self) -> Result<()> {
// Implementation of scanning directory and storing in Redis
}
pub fn page_get(&self, page_name: &str) -> Result<String> {
// Implementation of getting page content
}
// Additional methods for Collection
}
impl CollectionBuilder {
pub fn with_storage(mut self, storage: RedisStorage) -> Self {
self.storage = Some(storage);
self
}
pub fn build(self) -> Result<Collection> {
let storage = self.storage.ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::Other, "Storage not provided")
})?;
let collection = Collection {
path: self.path,
name: self.name,
storage,
};
Ok(collection)
}
}
DocTree Module (src/doctree.rs)
use std::collections::HashMap;
use std::path::Path;
use crate::collection::{Collection, CollectionBuilder};
use crate::error::{DocTreeError, Result};
use crate::storage::RedisStorage;
pub struct DocTree {
collections: HashMap<String, Collection>,
default_collection: Option<String>,
storage: RedisStorage,
}
pub struct DocTreeBuilder {
collections: HashMap<String, Collection>,
default_collection: Option<String>,
storage: Option<RedisStorage>,
}
impl DocTree {
pub fn builder() -> DocTreeBuilder {
DocTreeBuilder {
collections: HashMap::new(),
default_collection: None,
storage: None,
}
}
pub fn add_collection<P: AsRef<Path>>(&mut self, path: P, name: &str) -> Result<&Collection> {
// Implementation of adding a collection
}
// Additional methods for DocTree
}
impl DocTreeBuilder {
pub fn with_storage(mut self, storage: RedisStorage) -> Self {
self.storage = Some(storage);
self
}
pub fn with_collection<P: AsRef<Path>>(mut self, path: P, name: &str) -> Result<Self> {
// Implementation of adding a collection during building
Ok(self)
}
pub fn with_default_collection(mut self, name: &str) -> Self {
self.default_collection = Some(name.to_string());
self
}
pub fn build(self) -> Result<DocTree> {
let storage = self.storage.ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::Other, "Storage not provided")
})?;
let doctree = DocTree {
collections: self.collections,
default_collection: self.default_collection,
storage,
};
Ok(doctree)
}
}
Include Module (src/include.rs)
use crate::doctree::DocTree;
use crate::error::Result;
pub fn process_includes(content: &str, collection_name: &str, doctree: &DocTree) -> Result<String> {
// Implementation of processing include directives
}
fn parse_include_line(line: &str) -> Result<(Option<String>, Option<String>)> {
// Implementation of parsing include directives
}
fn handle_include(page_name: &str, collection_name: &str, doctree: &DocTree) -> Result<String> {
// Implementation of handling include directives
}
Main Library File (src/lib.rs)
mod error;
mod storage;
mod utils;
mod collection;
mod doctree;
mod include;
pub use error::{DocTreeError, Result};
pub use storage::RedisStorage;
pub use collection::{Collection, CollectionBuilder};
pub use doctree::{DocTree, DocTreeBuilder};
pub use include::process_includes;
CLI Example (doctreecmd/src/main.rs)
use clap::{App, Arg, SubCommand};
use doctree::{DocTree, RedisStorage};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let matches = App::new("DocTree CLI")
.version("0.1.0")
.author("Your Name")
.about("A tool to manage document collections")
.subcommand(
SubCommand::with_name("scan")
.about("Scan a directory and create a collection")
.arg(Arg::with_name("path").required(true))
.arg(Arg::with_name("name").required(true)),
)
.subcommand(
SubCommand::with_name("list")
.about("List collections"),
)
.subcommand(
SubCommand::with_name("get")
.about("Get page content")
.arg(Arg::with_name("collection").required(true))
.arg(Arg::with_name("page").required(true)),
)
.get_matches();
// Implementation of CLI commands
Ok(())
}
Example Usage
Here's how the library would be used with the builder pattern:
use doctree::{DocTree, RedisStorage};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create a Redis storage instance
let storage = RedisStorage::new("redis://localhost:6379")?;
// Create a DocTree instance using the builder pattern
let mut doctree = DocTree::builder()
.with_storage(storage.clone())
.with_collection("path/to/collection", "my-collection")?
.with_default_collection("my-collection")
.build()?;
// Get page content
let content = doctree.page_get("my-collection", "page-name")?;
println!("Page content: {}", content);
// Get HTML content
let html = doctree.page_get_html("my-collection", "page-name")?;
println!("HTML content: {}", html);
Ok(())
}
Testing Strategy
-
Unit Tests
- Test individual components in isolation
- Mock Redis for testing storage operations
- Test utility functions
-
Integration Tests
- Test the interaction between components
- Test the builder pattern
- Test include processing
-
End-to-End Tests
- Test the complete workflow with real files
- Test the CLI interface
Timeline
- Project Setup and Dependencies: 1 day
- Core Library Structure: 2 days
- Collection Implementation: 2 days
- DocTree Implementation: 2 days
- Include Processor Implementation: 1 day
- CLI Example: 1 day
- Testing and Documentation: 2 days
Total estimated time: 11 days