use crate::block::Block;
use crate::document::Document;
use crate::error::{BlocksError, Result};
use std::sync::Arc;
pub type MiddlewareFn = Arc<dyn Fn(&mut Block) -> Result<()> + Send + Sync>;
pub type DocumentMiddlewareFn = Arc<dyn Fn(&mut Document) -> Result<()> + Send + Sync>;
pub struct BlockPipeline {
middlewares: Vec<MiddlewareFn>,
name: String,
}
impl BlockPipeline {
pub fn new() -> Self {
Self {
middlewares: Vec::new(),
name: "default".to_string(),
}
}
pub fn with_name(name: impl Into<String>) -> Self {
Self {
middlewares: Vec::new(),
name: name.into(),
}
}
pub fn add_middleware<F>(mut self, middleware: F) -> Self
where
F: Fn(&mut Block) -> Result<()> + Send + Sync + 'static,
{
self.middlewares.push(Arc::new(middleware));
self
}
pub fn process(&self, block: &mut Block) -> Result<()> {
for middleware in &self.middlewares {
middleware(block)?;
}
Ok(())
}
pub fn process_all(&self, blocks: &mut [Block]) -> Result<()> {
for block in blocks {
self.process(block)?;
}
Ok(())
}
pub fn name(&self) -> &str {
&self.name
}
pub fn len(&self) -> usize {
self.middlewares.len()
}
pub fn is_empty(&self) -> bool {
self.middlewares.is_empty()
}
}
impl Default for BlockPipeline {
fn default() -> Self {
Self::new()
}
}
pub struct DocumentPipeline {
middlewares: Vec<DocumentMiddlewareFn>,
block_pipeline: Option<BlockPipeline>,
name: String,
}
impl DocumentPipeline {
pub fn new() -> Self {
Self {
middlewares: Vec::new(),
block_pipeline: None,
name: "default".to_string(),
}
}
pub fn with_name(name: impl Into<String>) -> Self {
Self {
middlewares: Vec::new(),
block_pipeline: None,
name: name.into(),
}
}
pub fn add_middleware<F>(mut self, middleware: F) -> Self
where
F: Fn(&mut Document) -> Result<()> + Send + Sync + 'static,
{
self.middlewares.push(Arc::new(middleware));
self
}
pub fn with_block_pipeline(mut self, pipeline: BlockPipeline) -> Self {
self.block_pipeline = Some(pipeline);
self
}
pub fn process(&self, doc: &mut Document) -> Result<()> {
for middleware in &self.middlewares {
middleware(doc)?;
}
if let Some(ref block_pipeline) = self.block_pipeline {
for block in &mut doc.blocks {
block_pipeline.process(block)?;
}
}
Ok(())
}
pub fn name(&self) -> &str {
&self.name
}
pub fn len(&self) -> usize {
self.middlewares.len()
}
pub fn is_empty(&self) -> bool {
self.middlewares.is_empty() && self.block_pipeline.is_none()
}
}
impl Default for DocumentPipeline {
fn default() -> Self {
Self::new()
}
}
pub mod block_middlewares {
use super::*;
use crate::BlockType;
pub fn trim_content() -> impl Fn(&mut Block) -> Result<()> + Send + Sync + 'static {
|block| {
block.content = block.content.trim().to_string();
Ok(())
}
}
pub fn uppercase() -> impl Fn(&mut Block) -> Result<()> + Send + Sync + 'static {
|block| {
block.content = block.content.to_uppercase();
Ok(())
}
}
pub fn lowercase() -> impl Fn(&mut Block) -> Result<()> + Send + Sync + 'static {
|block| {
block.content = block.content.to_lowercase();
Ok(())
}
}
pub fn add_prefix(prefix: String) -> impl Fn(&mut Block) -> Result<()> + Send + Sync + 'static {
move |block| {
if matches!(block.block_type, BlockType::Text) {
block.content = format!("{}{}", prefix, block.content);
}
Ok(())
}
}
pub fn add_suffix(suffix: String) -> impl Fn(&mut Block) -> Result<()> + Send + Sync + 'static {
move |block| {
if matches!(block.block_type, BlockType::Text) {
block.content = format!("{}{}", block.content, suffix);
}
Ok(())
}
}
pub fn require_content() -> impl Fn(&mut Block) -> Result<()> + Send + Sync + 'static {
|block| {
if block.content.trim().is_empty() {
return Err(BlocksError::EmptyContent {
block_type: block.type_name().to_string(),
});
}
Ok(())
}
}
pub fn max_length(max: usize) -> impl Fn(&mut Block) -> Result<()> + Send + Sync + 'static {
move |block| {
if block.content.len() > max {
block.content = block.content.chars().take(max).collect();
}
Ok(())
}
}
pub fn replace_text(
from: String,
to: String,
) -> impl Fn(&mut Block) -> Result<()> + Send + Sync + 'static {
move |block| {
block.content = block.content.replace(&from, &to);
Ok(())
}
}
pub fn add_metadata(
key: String,
value: String,
) -> impl Fn(&mut Block) -> Result<()> + Send + Sync + 'static {
move |block| {
block.metadata.insert(key.clone(), value.clone());
Ok(())
}
}
pub fn timestamp_metadata() -> impl Fn(&mut Block) -> Result<()> + Send + Sync + 'static {
|block| {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
block
.metadata
.insert("processed_at".to_string(), now.to_string());
Ok(())
}
}
}
pub mod document_middlewares {
use super::*;
pub fn default_title(
title: String,
) -> impl Fn(&mut Document) -> Result<()> + Send + Sync + 'static {
move |doc| {
if doc.title.is_empty() {
doc.title = title.clone();
}
Ok(())
}
}
pub fn add_metadata(
key: String,
value: String,
) -> impl Fn(&mut Document) -> Result<()> + Send + Sync + 'static {
move |doc| {
doc.metadata.insert(key.clone(), value.clone());
Ok(())
}
}
pub fn require_blocks() -> impl Fn(&mut Document) -> Result<()> + Send + Sync + 'static {
|doc| {
if doc.blocks.is_empty() {
return Err(BlocksError::ValidationError {
message: "Document must have at least one block".to_string(),
});
}
Ok(())
}
}
pub fn remove_empty_blocks() -> impl Fn(&mut Document) -> Result<()> + Send + Sync + 'static {
|doc| {
doc.blocks.retain(|block| !block.content.trim().is_empty());
Ok(())
}
}
pub fn max_blocks(max: usize) -> impl Fn(&mut Document) -> Result<()> + Send + Sync + 'static {
move |doc| {
if doc.blocks.len() > max {
doc.blocks.truncate(max);
}
Ok(())
}
}
pub fn timestamp_metadata() -> impl Fn(&mut Document) -> Result<()> + Send + Sync + 'static {
|doc| {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
doc.metadata
.insert("processed_at".to_string(), now.to_string());
Ok(())
}
}
pub fn sort_by_type() -> impl Fn(&mut Document) -> Result<()> + Send + Sync + 'static {
|doc| {
doc.blocks.sort_by(|a, b| a.type_name().cmp(b.type_name()));
Ok(())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::BlockType;
#[test]
fn test_block_pipeline_creation() {
let pipeline = BlockPipeline::new();
assert!(pipeline.is_empty());
assert_eq!(pipeline.len(), 0);
}
#[test]
fn test_block_pipeline_with_middleware() {
let pipeline = BlockPipeline::new()
.add_middleware(|block| {
block.content = block.content.trim().to_string();
Ok(())
})
.add_middleware(|block| {
block.content = block.content.to_uppercase();
Ok(())
});
assert_eq!(pipeline.len(), 2);
}
#[test]
fn test_block_pipeline_processing() {
let pipeline = BlockPipeline::new()
.add_middleware(block_middlewares::trim_content())
.add_middleware(block_middlewares::uppercase());
let mut block = Block::new(BlockType::Text, " hello world ".to_string());
pipeline.process(&mut block).unwrap();
assert_eq!(block.content, "HELLO WORLD");
}
#[test]
fn test_document_pipeline_creation() {
let pipeline = DocumentPipeline::new();
assert!(pipeline.is_empty());
}
#[test]
fn test_document_pipeline_processing() {
let block_pipeline = BlockPipeline::new().add_middleware(block_middlewares::trim_content());
let doc_pipeline = DocumentPipeline::new()
.add_middleware(document_middlewares::default_title("Untitled".to_string()))
.with_block_pipeline(block_pipeline);
let mut doc = Document::new();
doc.add_block(Block::new(BlockType::Text, " content ".to_string()));
doc_pipeline.process(&mut doc).unwrap();
assert_eq!(doc.title, "Untitled");
assert_eq!(doc.blocks[0].content, "content");
}
#[test]
fn test_block_middlewares() {
let mut block = Block::new(BlockType::Text, "Hello".to_string());
let add_prefix = block_middlewares::add_prefix("[PREFIX] ".to_string());
add_prefix(&mut block).unwrap();
assert!(block.content.starts_with("[PREFIX]"));
let mut long_block = Block::new(BlockType::Text, "a".repeat(1000));
let max_length = block_middlewares::max_length(100);
max_length(&mut long_block).unwrap();
assert_eq!(long_block.content.len(), 100);
}
#[test]
fn test_document_middlewares() {
let mut doc = Document::new();
doc.add_block(Block::new(BlockType::Text, "".to_string()));
doc.add_block(Block::new(BlockType::Text, "content".to_string()));
doc.add_block(Block::new(BlockType::Text, " ".to_string()));
let remove_empty = document_middlewares::remove_empty_blocks();
remove_empty(&mut doc).unwrap();
assert_eq!(doc.blocks.len(), 1);
assert_eq!(doc.blocks[0].content, "content");
}
#[test]
fn test_pipeline_with_name() {
let pipeline = BlockPipeline::with_name("my-pipeline");
assert_eq!(pipeline.name(), "my-pipeline");
}
#[test]
fn test_process_all_blocks() {
let pipeline = BlockPipeline::new().add_middleware(block_middlewares::uppercase());
let mut blocks = vec![
Block::new(BlockType::Text, "hello".to_string()),
Block::new(BlockType::Text, "world".to_string()),
];
pipeline.process_all(&mut blocks).unwrap();
assert_eq!(blocks[0].content, "HELLO");
assert_eq!(blocks[1].content, "WORLD");
}
#[test]
fn test_require_content_middleware() {
let require = block_middlewares::require_content();
let mut valid = Block::new(BlockType::Text, "content".to_string());
assert!(require(&mut valid).is_ok());
let mut empty = Block::new(BlockType::Text, " ".to_string());
assert!(require(&mut empty).is_err());
}
#[test]
fn test_metadata_middleware() {
let add_meta = block_middlewares::add_metadata("key".to_string(), "value".to_string());
let mut block = Block::new(BlockType::Text, "test".to_string());
add_meta(&mut block).unwrap();
assert_eq!(block.metadata.get("key"), Some(&"value".to_string()));
}
}