#![doc = include_str!("../README.md")]
#![doc(
html_favicon_url = "https://kura.pro/nucleusflow/images/favicon.ico",
html_logo_url = "https://kura.pro/nucleusflow/images/logos/nucleusflow.svg",
html_root_url = "https://docs.rs/nucleusflow"
)]
#![crate_name = "nucleusflow"]
#![crate_type = "lib"]
use crate::core::error::{ProcessingError, Result};
use crate::core::traits::Generator;
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
pub mod core {
pub mod config;
pub mod error;
pub mod traits;
}
pub mod cli;
pub mod generators;
pub mod process;
pub mod processors;
pub mod template;
pub trait ContentProcessor: Send + Sync + std::fmt::Debug {
fn process(
&self,
content: &str,
context: Option<&serde_json::Value>,
) -> Result<String>;
fn validate(&self, content: &str) -> Result<()>;
}
pub trait TemplateRenderer: Send + Sync + std::fmt::Debug {
fn render(
&self,
template: &str,
context: &serde_json::Value,
) -> Result<String>;
fn validate(
&self,
template: &str,
context: &serde_json::Value,
) -> Result<()>;
}
#[derive(Debug)]
pub struct FileContentProcessor {
pub base_path: PathBuf,
}
impl FileContentProcessor {
pub fn new(base_path: PathBuf) -> Self {
Self { base_path }
}
}
impl ContentProcessor for FileContentProcessor {
fn process(
&self,
content: &str,
_context: Option<&serde_json::Value>,
) -> Result<String> {
Ok(content.to_uppercase())
}
fn validate(&self, _content: &str) -> Result<()> {
Ok(())
}
}
#[derive(Debug)]
pub struct HtmlTemplateRenderer {
pub base_path: PathBuf,
}
impl HtmlTemplateRenderer {
pub fn new(base_path: PathBuf) -> Self {
Self { base_path }
}
}
impl TemplateRenderer for HtmlTemplateRenderer {
fn render(
&self,
_template: &str,
context: &serde_json::Value,
) -> Result<String> {
Ok(format!(
"<html>{}</html>",
context["content"].as_str().unwrap_or("")
))
}
fn validate(
&self,
_template: &str,
_context: &serde_json::Value,
) -> Result<()> {
Ok(())
}
}
#[derive(Debug)]
pub struct HtmlOutputGenerator {
pub base_path: PathBuf,
}
impl HtmlOutputGenerator {
pub fn new(base_path: PathBuf) -> Self {
Self { base_path }
}
}
impl Generator for HtmlOutputGenerator {
fn generate(
&self,
content: &str,
path: &Path,
_options: Option<&serde_json::Value>,
) -> Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| {
ProcessingError::io_error(parent.to_path_buf(), e)
})?;
}
let mut file = fs::File::create(path)?;
file.write_all(content.as_bytes())?;
Ok(())
}
fn validate(
&self,
path: &Path,
_options: Option<&serde_json::Value>,
) -> Result<()> {
if let Some(parent) = path.parent() {
if !parent.exists() {
fs::create_dir_all(parent).map_err(|e| {
ProcessingError::io_error(parent.to_path_buf(), e)
})?;
}
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct NucleusFlowConfig {
pub content_dir: PathBuf,
pub output_dir: PathBuf,
pub template_dir: PathBuf,
}
impl NucleusFlowConfig {
pub fn new<P: AsRef<Path>>(
content_dir: P,
output_dir: P,
template_dir: P,
) -> Result<Self> {
let content_dir = content_dir.as_ref().to_path_buf();
let output_dir = output_dir.as_ref().to_path_buf();
let template_dir = template_dir.as_ref().to_path_buf();
for dir in [&content_dir, &template_dir] {
if !dir.exists() || !dir.is_dir() {
return Err(ProcessingError::configuration(
"Invalid directory",
Some(dir.clone()),
None,
));
}
}
if !output_dir.exists() {
fs::create_dir_all(&output_dir).map_err(|e| {
ProcessingError::configuration(
format!("Failed to create output directory: {}", e),
Some(output_dir.clone()),
None,
)
})?;
}
Ok(Self {
content_dir,
output_dir,
template_dir,
})
}
}
#[derive(Debug)]
pub struct NucleusFlow {
config: NucleusFlowConfig,
content_processor: Box<dyn ContentProcessor>,
template_renderer: Box<dyn TemplateRenderer>,
output_generator: Box<dyn Generator>,
}
impl NucleusFlow {
pub fn new(
config: NucleusFlowConfig,
content_processor: Box<dyn ContentProcessor>,
template_renderer: Box<dyn TemplateRenderer>,
output_generator: Box<dyn Generator>,
) -> Self {
Self {
config,
content_processor,
template_renderer,
output_generator,
}
}
pub fn process(&self) -> Result<()> {
for entry in fs::read_dir(&self.config.content_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
self.process_file(&path)?;
}
}
Ok(())
}
fn process_file(&self, path: &Path) -> Result<()> {
let content = fs::read_to_string(path)?;
let processed =
self.content_processor.process(&content, None)?;
let context =
serde_json::json!({ "content": processed, "path": path });
let template_name = "default";
let rendered =
self.template_renderer.render(template_name, &context)?;
let relative_path = path
.strip_prefix(&self.config.content_dir)
.map_err(|e| ProcessingError::ContentProcessing {
details: format!(
"Failed to determine relative path: {}",
e
),
source: None,
})?;
let output_path = self
.config
.output_dir
.join(relative_path)
.with_extension("html");
self.output_generator.generate(
&rendered,
&output_path,
None,
)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_nucleus_flow_config_new() {
let temp_dir = TempDir::new().unwrap();
let content_path = temp_dir.path().join("content");
let output_path = temp_dir.path().join("output");
let template_path = temp_dir.path().join("templates");
fs::create_dir(&content_path).unwrap();
fs::create_dir(&template_path).unwrap();
let config = NucleusFlowConfig::new(
&content_path,
&output_path,
&template_path,
)
.unwrap();
assert_eq!(config.content_dir, content_path);
assert_eq!(config.output_dir, output_path);
assert_eq!(config.template_dir, template_path);
}
#[test]
fn test_nucleus_flow_process() -> Result<()> {
let temp_dir = TempDir::new().unwrap();
let content_path = temp_dir.path().join("content");
let output_path = temp_dir.path().join("output");
let template_path = temp_dir.path().join("templates");
fs::create_dir(&content_path)?;
fs::create_dir(&template_path)?;
let test_content = "test content";
let content_file = content_path.join("test.txt");
fs::write(&content_file, test_content)?;
let config = NucleusFlowConfig::new(
&content_path,
&output_path,
&template_path,
)?;
let nucleus = NucleusFlow::new(
config,
Box::new(FileContentProcessor::new(content_path.clone())),
Box::new(HtmlTemplateRenderer::new(template_path.clone())),
Box::new(HtmlOutputGenerator::new(output_path.clone())),
);
nucleus.process()?;
let output_file = output_path.join("test.html");
assert!(output_file.exists());
let output_content = fs::read_to_string(output_file)?;
assert_eq!(output_content, "<html>TEST CONTENT</html>");
Ok(())
}
}