use async_trait::async_trait;
use std::io;
use std::path::{Path, PathBuf};
pub mod bundle;
pub mod compress;
pub mod minify;
#[cfg(feature = "advanced-minification")]
pub mod advanced_minify;
#[cfg(feature = "image-optimization")]
pub mod images;
#[cfg(feature = "source-maps")]
pub mod sourcemap;
pub type ProcessingResult<T> = io::Result<T>;
#[async_trait]
pub trait Processor: Send + Sync {
async fn process(&self, input: &[u8], path: &Path) -> ProcessingResult<Vec<u8>>;
fn can_process(&self, path: &Path) -> bool;
fn name(&self) -> &str;
}
#[derive(Debug, Clone)]
pub struct ProcessingConfig {
pub minify: bool,
pub source_maps: bool,
pub optimize_images: bool,
pub bundle: bool,
pub output_dir: PathBuf,
pub image_quality: u8,
}
impl Default for ProcessingConfig {
fn default() -> Self {
Self {
minify: true,
source_maps: false,
optimize_images: true,
bundle: false,
output_dir: PathBuf::from("static/processed"),
image_quality: 85,
}
}
}
impl ProcessingConfig {
pub fn new(output_dir: PathBuf) -> Self {
Self {
output_dir,
..Default::default()
}
}
pub fn with_minification(mut self, enable: bool) -> Self {
self.minify = enable;
self
}
pub fn with_source_maps(mut self, enable: bool) -> Self {
self.source_maps = enable;
self
}
pub fn with_image_optimization(mut self, enable: bool) -> Self {
self.optimize_images = enable;
self
}
pub fn with_image_quality(mut self, quality: u8) -> Self {
self.image_quality = quality.clamp(1, 100);
self
}
}
pub struct ProcessingPipeline {
config: ProcessingConfig,
processors: Vec<Box<dyn Processor>>,
}
impl ProcessingPipeline {
pub fn new(config: ProcessingConfig) -> Self {
let mut processors: Vec<Box<dyn Processor>> = Vec::new();
if config.minify {
processors.push(Box::new(minify::CssMinifier::new()));
processors.push(Box::new(minify::JsMinifier::new()));
}
#[cfg(feature = "image-optimization")]
if config.optimize_images {
processors.push(Box::new(images::ImageOptimizer::new(config.image_quality)));
}
Self { config, processors }
}
pub async fn process_file(&self, input: &[u8], path: &Path) -> ProcessingResult<Vec<u8>> {
let mut content = input.to_vec();
for processor in &self.processors {
if processor.can_process(path) {
content = processor.process(&content, path).await?;
}
}
Ok(content)
}
pub fn config(&self) -> &ProcessingConfig {
&self.config
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_processing_config_default() {
let config = ProcessingConfig::default();
assert!(config.minify);
assert!(!config.source_maps);
assert!(config.optimize_images);
assert!(!config.bundle);
assert_eq!(config.image_quality, 85);
}
#[test]
fn test_processing_config_builder() {
let config = ProcessingConfig::new(PathBuf::from("dist"))
.with_minification(false)
.with_source_maps(true)
.with_image_optimization(false)
.with_image_quality(90);
assert!(!config.minify);
assert!(config.source_maps);
assert!(!config.optimize_images);
assert_eq!(config.image_quality, 90);
}
#[test]
fn test_image_quality_clamping() {
let config1 = ProcessingConfig::default().with_image_quality(150);
assert_eq!(config1.image_quality, 100);
let config2 = ProcessingConfig::default().with_image_quality(0);
assert_eq!(config2.image_quality, 1);
}
#[test]
fn test_pipeline_creation() {
let config = ProcessingConfig::new(PathBuf::from("dist"));
let pipeline = ProcessingPipeline::new(config);
assert!(pipeline.config().minify);
}
#[tokio::test]
async fn test_pipeline_process_empty() {
let config = ProcessingConfig::new(PathBuf::from("dist"));
let pipeline = ProcessingPipeline::new(config);
let result = pipeline
.process_file(b"test", &PathBuf::from("test.txt"))
.await
.unwrap();
assert_eq!(result, b"test");
}
}