//! Pipeline infrastructure for orchestrating crate generation stages
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use tracing::{debug, info, warn};
/// Context object that carries state through the crate generation pipeline
/// Contains all information needed to generate a complete Rust crate
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CrateContext {
/// The name of the crate being generated
pub crate_name: String,
/// Version string for the crate (defaults to "0.1.0")
pub version: String,
/// Human-readable description of what the crate does
pub description: String,
/// Filesystem path where the crate will be generated
pub output_path: PathBuf,
/// Map of file paths to their generated content
pub generated_files: HashMap<String, String>,
/// List of Cargo features to include in the crate
pub features: Vec<String>,
/// List of dependencies in Cargo.toml format
pub dependencies: Vec<String>,
/// Key-value metadata for the crate (author, license, etc.)
pub metadata: HashMap<String, serde_json::Value>,
/// Outputs from each pipeline stage for debugging and analysis
pub stage_outputs: HashMap<String, serde_json::Value>,
}
impl CrateContext {
/// Creates a new crate context from an initial idea or description
///
/// # Arguments
/// * `idea` - The initial concept or description for the crate
/// * `output_dir` - Optional directory where the crate should be generated
///
/// # Returns
/// A new `CrateContext` with sensible defaults
pub fn new(idea: &str, output_dir: Option<&std::path::Path>) -> Self {
let crate_name = Self::generate_crate_name(idea);
let output_path = output_dir.map_or_else(
|| PathBuf::from(format!("./{crate_name}")),
std::path::Path::to_path_buf,
);
debug!("Creating new crate context for: {}", crate_name);
Self {
crate_name,
version: "0.1.0".to_string(),
description: idea.to_string(),
output_path,
generated_files: HashMap::new(),
dependencies: Vec::new(),
features: Vec::new(),
metadata: HashMap::new(),
stage_outputs: HashMap::new(),
}
}
/// Generates a valid crate name from an idea or description
///
/// # Arguments
/// * `idea` - The source text to convert to a crate name
///
/// # Returns
/// A valid Rust crate name following naming conventions
fn generate_crate_name(idea: &str) -> String {
let name: String = idea
.split_whitespace()
.take(3)
.map(|word| {
word.chars()
.filter(|c| c.is_alphanumeric())
.collect::<String>()
})
.filter(|word| !word.is_empty())
.collect::<Vec<_>>()
.join("_")
.to_lowercase()
.chars()
.take(20)
.collect();
// Ensure we have a valid name
if name.is_empty() {
"generated_crate".to_string()
} else {
name
}
}
/// Adds a generated file to the context
///
/// # Arguments
/// * `path` - Relative path for the file within the crate
/// * `content` - The file content as a string
pub fn add_file(&mut self, path: &str, content: String) {
debug!("Adding file to context: {}", path);
self.generated_files.insert(path.to_string(), content);
}
/// Adds a dependency to the crate if it doesn't already exist
///
/// # Arguments
/// * `dep` - Dependency specification in Cargo.toml format (e.g., "serde = \"1.0\"")
pub fn add_dependency(&mut self, dep: String) {
if !self.dependencies.contains(&dep) {
debug!("Adding dependency: {}", dep);
self.dependencies.push(dep);
}
}
/// Adds a feature to the crate if it doesn't already exist
///
/// # Arguments
/// * `feature` - Name of the Cargo feature to add
pub fn add_feature(&mut self, feature: String) {
if !self.features.contains(&feature) {
debug!("Adding feature: {}", feature);
self.features.push(feature);
}
}
/// Sets a metadata key-value pair for the crate
///
/// # Arguments
/// * `key` - The metadata key (e.g., "author", "license")
/// * `value` - The metadata value
pub fn set_metadata(&mut self, key: String, value: String) {
debug!("Setting metadata: {} = {}", key, value);
self.metadata.insert(key, value.into());
}
/// Records the output of a pipeline stage for debugging and analysis
///
/// # Arguments
/// * `stage` - Name of the pipeline stage
/// * `output` - The stage's output data
pub fn set_stage_output(&mut self, stage: &str, output: serde_json::Value) {
debug!("Recording output for stage: {}", stage);
self.stage_outputs.insert(stage.to_string(), output);
}
/// Gets the list of all generated file paths
///
/// # Returns
/// A vector of file paths that will be generated
#[must_use]
pub fn get_file_paths(&self) -> Vec<String> {
self.generated_files.keys().cloned().collect()
}
/// Gets the content of a specific generated file
///
/// # Arguments
/// * `path` - The file path to retrieve
///
/// # Returns
/// The file content if it exists
#[must_use]
pub fn get_file_content(&self, path: &str) -> Option<&String> {
self.generated_files.get(path)
}
/// Validates that the context has the minimum required information
///
/// # Returns
/// True if the context is valid for crate generation
#[must_use]
pub fn is_valid(&self) -> bool {
!self.crate_name.is_empty() && !self.description.is_empty() && !self.version.is_empty()
}
/// Gets a summary of the context for logging or display
///
/// # Returns
/// A formatted string with key context information
#[must_use]
pub fn summary(&self) -> String {
format!(
"Crate: {} v{}\nDescription: {}\nFiles: {}\nDependencies: {}\nFeatures: {}",
self.crate_name,
self.version,
self.description,
self.generated_files.len(),
self.dependencies.len(),
self.features.len()
)
}
}
/// Main pipeline orchestrator for crate generation
/// Coordinates the execution of different generation stages
pub struct GenerationPipeline {
/// Whether to enable verbose logging during pipeline execution
verbose: bool,
}
impl GenerationPipeline {
/// Creates a new generation pipeline
///
/// # Arguments
/// * `verbose` - Whether to enable detailed logging
///
/// # Returns
/// A new `GenerationPipeline` instance
#[must_use]
pub fn new(verbose: bool) -> Self {
Self { verbose }
}
/// Creates a pipeline with default settings
///
/// # Returns
/// A new `GenerationPipeline` with verbose logging disabled
#[must_use]
pub fn default() -> Self {
Self::new(false)
}
/// Executes the complete crate generation pipeline
///
/// # Arguments
/// * `context` - The initial crate context
///
/// # Returns
/// The final context with all generated content
///
/// # Errors
/// Returns an error if any pipeline stage fails
pub async fn run(&self, mut context: CrateContext) -> Result<CrateContext> {
info!(
"Starting crate generation pipeline for: {}",
context.crate_name
);
if !context.is_valid() {
return Err(anyhow::anyhow!("Invalid context: missing required fields"));
}
// Stage 1: Validation and setup
context = self.validate_and_setup(context).await?;
// Stage 2: Generate basic project structure
context = self.generate_structure(context).await?;
// Stage 3: Generate Cargo.toml
context = self.generate_cargo_toml(context).await?;
// Stage 4: Generate main library files
context = self.generate_lib_files(context).await?;
// Stage 5: Generate tests
context = self.generate_tests(context).await?;
// Stage 6: Generate documentation
context = self.generate_docs(context).await?;
// Stage 7: Final validation
context = self.final_validation(context).await?;
info!(
"Pipeline completed successfully for: {}",
context.crate_name
);
if self.verbose {
info!("Final context summary:\n{}", context.summary());
}
Ok(context)
}
/// Validates the context and sets up default values
async fn validate_and_setup(&self, mut context: CrateContext) -> Result<CrateContext> {
debug!("Stage 1: Validation and setup");
// Set default metadata if not provided
if context.metadata.get("author").is_none() {
context.set_metadata("author".to_string(), "OpenCrates Generator".to_string());
}
if context.metadata.get("license").is_none() {
context.set_metadata("license".to_string(), "MIT OR Apache-2.0".to_string());
}
if context.metadata.get("edition").is_none() {
context.set_metadata("edition".to_string(), "2021".to_string());
}
context.set_stage_output(
"validation",
serde_json::json!({
"status": "completed",
"metadata_count": context.metadata.len()
}),
);
Ok(context)
}
/// Generates the basic project directory structure
async fn generate_structure(&self, mut context: CrateContext) -> Result<CrateContext> {
debug!("Stage 2: Generating project structure");
// Create basic directory structure files
context.add_file(".gitignore", self.generate_gitignore());
context.add_file("README.md", self.generate_readme(&context));
context.add_file("LICENSE-MIT", self.generate_mit_license());
context.add_file("LICENSE-APACHE", self.generate_apache_license());
context.set_stage_output(
"structure",
serde_json::json!({
"status": "completed",
"files_created": ["gitignore", "readme", "licenses"]
}),
);
Ok(context)
}
/// Generates the Cargo.toml file
async fn generate_cargo_toml(&self, mut context: CrateContext) -> Result<CrateContext> {
debug!("Stage 3: Generating Cargo.toml");
let cargo_toml = self.generate_cargo_toml_content(&context);
context.add_file("Cargo.toml", cargo_toml);
context.set_stage_output(
"cargo_toml",
serde_json::json!({
"status": "completed",
"dependencies_count": context.dependencies.len(),
"features_count": context.features.len()
}),
);
Ok(context)
}
/// Generates the main library files
async fn generate_lib_files(&self, mut context: CrateContext) -> Result<CrateContext> {
debug!("Stage 4: Generating library files");
// Generate lib.rs
let lib_content = self.generate_lib_rs(&context);
context.add_file("src/lib.rs", lib_content);
// Generate main.rs if it's a binary crate
if context.metadata.get("type").map(|v| v.as_str()) == Some(Some("bin")) {
let main_content = self.generate_main_rs(&context);
context.add_file("src/main.rs", main_content);
}
context.set_stage_output("lib_files", serde_json::json!({
"status": "completed",
"lib_generated": true,
"main_generated": context.metadata.get("type").map(|v| v.as_str()) == Some(Some("bin"))
}));
Ok(context)
}
/// Generates test files
async fn generate_tests(&self, mut context: CrateContext) -> Result<CrateContext> {
debug!("Stage 5: Generating tests");
let test_content = self.generate_test_file(&context);
context.add_file("tests/integration_tests.rs", test_content);
context.set_stage_output(
"tests",
serde_json::json!({
"status": "completed",
"test_files": ["integration_tests.rs"]
}),
);
Ok(context)
}
/// Generates documentation files
async fn generate_docs(&self, mut context: CrateContext) -> Result<CrateContext> {
debug!("Stage 6: Generating documentation");
let changelog = self.generate_changelog(&context);
context.add_file("CHANGELOG.md", changelog);
context.set_stage_output(
"docs",
serde_json::json!({
"status": "completed",
"docs_generated": ["changelog"]
}),
);
Ok(context)
}
/// Performs final validation of the generated content
async fn final_validation(&self, mut context: CrateContext) -> Result<CrateContext> {
debug!("Stage 7: Final validation");
// Validate that essential files exist
let required_files = ["Cargo.toml", "src/lib.rs", "README.md"];
let mut missing_files = Vec::new();
for file in required_files {
if !context.generated_files.contains_key(file) {
missing_files.push(file);
}
}
if !missing_files.is_empty() {
warn!("Missing required files: {:?}", missing_files);
}
context.set_stage_output(
"final_validation",
serde_json::json!({
"status": "completed",
"total_files": context.generated_files.len(),
"missing_files": missing_files,
"valid": missing_files.is_empty()
}),
);
Ok(context)
}
// Helper methods for generating file content
fn generate_gitignore(&self) -> String {
"/target\n**/*.rs.bak\nCargo.lock\n.DS_Store\n*.swp\n*.swo\n*~\n".to_string()
}
fn generate_readme(&self, context: &CrateContext) -> String {
format!(
"# {}\n\n{}\n\n## Installation\n\n```toml\n[dependencies]\n{} = \"{}\"\n```\n\n## Usage\n\n```rust\nuse {};\n```\n\n## License\n\nLicensed under either of\n\n* Apache License, Version 2.0\n* MIT license\n\nat your option.\n",
context.crate_name,
context.description,
context.crate_name,
context.version,
context.crate_name.replace('-', "_")
)
}
fn generate_mit_license(&self) -> String {
"MIT License\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIability, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.".to_string()
}
fn generate_apache_license(&self) -> String {
"Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nCopyright [yyyy] [name of copyright owner]\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed under an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.".to_string()
}
fn generate_cargo_toml_content(&self, ctx: &CrateContext) -> String {
let mut cargo_content =
format!(
"[package]\nname = \"{}\"\nversion = \"{}\"\nedition = \"{}\"\ndescription = \"{}\"\n",
ctx.crate_name,
ctx.version,
ctx.metadata.get("edition").and_then(|v| v.as_str()).unwrap_or("2021"),
ctx.description
);
if let Some(author) = ctx.metadata.get("author").and_then(|v| v.as_str()) {
cargo_content.push_str(&format!("authors = [\"{author}\"]\n"));
}
if let Some(license) = ctx.metadata.get("license").and_then(|v| v.as_str()) {
cargo_content.push_str(&format!("license = \"{license}\"\n"));
}
if !ctx.dependencies.is_empty() {
cargo_content.push_str("\n[dependencies]\n");
for dep in &ctx.dependencies {
cargo_content.push_str(&format!("{dep}\n"));
}
}
if !ctx.features.is_empty() {
cargo_content.push_str("\n[features]\n");
for feature in &ctx.features {
cargo_content.push_str(&format!("{feature} = []\n"));
}
}
cargo_content
}
fn generate_lib_rs(&self, context: &CrateContext) -> String {
format!(
"//! {}\n//!\n//! This crate provides functionality for {}.\n\n/// Main functionality for {}\npub fn hello() -> String {{\n \"Hello from {}!\".to_string()\n}}\n\n#[cfg(test)]\nmod tests {{\n use super::*;\n\n #[test]\n fn test_hello() {{\n assert_eq!(hello(), \"Hello from {}!\");\n }}\n}}\n",
context.crate_name,
context.description.to_lowercase(),
context.crate_name,
context.crate_name,
context.crate_name
)
}
fn generate_main_rs(&self, context: &CrateContext) -> String {
format!(
"//! Main entry point for {}\n\nfn main() {{\n println!(\"{}\");\n println!(\"Description: {}\");\n}}\n",
context.crate_name,
context.crate_name,
context.description
)
}
fn generate_test_file(&self, context: &CrateContext) -> String {
format!(
"//! Integration tests for {}\n\nuse {};\n\n#[test]\nfn test_basic_functionality() {{\n // Add your integration tests here\n assert!(true);\n}}\n",
context.crate_name,
context.crate_name.replace('-', "_")
)
}
fn generate_changelog(&self, context: &CrateContext) -> String {
format!(
"# Changelog\n\nAll notable changes to {} will be documented in this file.\n\n## [{}] - {}\n\n### Added\n- Initial release\n- {}\n",
context.crate_name,
context.version,
chrono::Utc::now().format("%Y-%m-%d"),
context.description
)
}
}
impl Default for GenerationPipeline {
fn default() -> Self {
Self::new(false)
}
}
/// Convenience function to run the complete pipeline with default settings
///
/// # Arguments
/// * `context` - The initial crate context
///
/// # Returns
/// The completed context with all generated files
///
/// # Errors
/// Returns an error if pipeline execution fails
pub async fn run_pipeline(context: CrateContext) -> Result<CrateContext> {
let pipeline = GenerationPipeline::default();
pipeline.run(context).await
}
/// Runs the pipeline with verbose logging enabled
///
/// # Arguments
/// * `context` - The initial crate context
///
/// # Returns
/// The completed context with all generated files
///
/// # Errors
/// Returns an error if pipeline execution fails
pub async fn run_pipeline_verbose(context: CrateContext) -> Result<CrateContext> {
let pipeline = GenerationPipeline::new(true);
pipeline.run(context).await
}