use anyhow::{Result, Context};
use std::fs;
use std::path::PathBuf;
use walkdir::WalkDir;
pub fn format_pipeline(file: Option<PathBuf>) -> Result<()> {
let files = if let Some(ref f) = file {
vec![f.clone()]
} else {
let mut files = Vec::new();
for entry in WalkDir::new("src")
.into_iter()
.filter_map(|e| e.ok())
{
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("rs") {
files.push(path.to_path_buf());
}
}
files
};
let mut changed = 0;
for file in files {
if let Ok(content) = fs::read_to_string(&file) {
if content.contains("pipeline!") {
let formatted = format_file(&content)?;
if formatted != content {
fs::write(&file, &formatted)?;
println!("📝 Formatted {}", file.display());
changed += 1;
}
}
}
}
if changed == 0 {
println!("✅ All pipeline files are formatted");
} else {
println!("✨ Formatted {} file(s)", changed);
}
Ok(())
}
fn format_file(content: &str) -> Result<String> {
let mut result = String::new();
let mut lines = content.lines().peekable();
while let Some(line) = lines.next() {
if line.trim().starts_with("pipeline!") {
let mut block = line.to_string();
let mut brace_count = block.matches('{').count() - block.matches('}').count();
while brace_count > 0 {
if let Some(next) = lines.next() {
block.push('\n');
block.push_str(next);
brace_count += next.matches('{').count();
brace_count -= next.matches('}').count();
} else {
anyhow::bail!("Unclosed pipeline! macro");
}
}
result.push_str(&reformat_pipeline_block(&block)?);
result.push('\n');
} else {
result.push_str(line);
result.push('\n');
}
}
Ok(result)
}
fn reformat_pipeline_block(block: &str) -> Result<String> {
let start = block.find('{').ok_or_else(|| anyhow::anyhow!("no opening brace"))? + 1;
let end = block.rfind('}').ok_or_else(|| anyhow::anyhow!("no closing brace"))?;
let inner = &block[start..end];
let mut formatted = String::new();
formatted.push_str("pipeline! {\n");
let mut current = String::new();
let mut paren_depth = 0;
let mut bracket_depth = 0;
for ch in inner.chars() {
match ch {
'(' => paren_depth += 1,
')' => paren_depth -= 1,
'[' => bracket_depth += 1,
']' => bracket_depth -= 1,
',' if paren_depth == 0 && bracket_depth == 0 => {
let trimmed = current.trim();
if !trimmed.is_empty() {
formatted.push_str(" ");
formatted.push_str(trimmed);
formatted.push_str(",\n");
}
current.clear();
continue;
}
_ => {}
}
current.push(ch);
}
let trimmed = current.trim();
if !trimmed.is_empty() {
formatted.push_str(" ");
formatted.push_str(trimmed);
formatted.push('\n');
}
formatted.push('}');
Ok(formatted)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_reformat_pipeline_block() -> Result<()> {
let input = "pipeline! {\nname: MyPipe,\ninput: i32,\nsteps: [ step(\"a\") { a+1 }, step(\"b\") { b*2 } ],\nconstraints: [ metric(\"out\").ge(0) ]\n}";
let output = reformat_pipeline_block(input)?;
assert!(output.contains("pipeline! {"));
assert!(output.contains(" name: MyPipe,"));
assert!(output.contains(" input: i32,"));
assert!(output.contains(" steps: ["));
assert!(output.contains(" constraints: ["));
Ok(())
}
}