use anyhow::{Context, Result, anyhow};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
use toml::Table;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
pub source_dirs: Vec<String>,
pub output_file: String,
pub layout: String,
pub exclude_patterns: Vec<String>,
pub layers: Vec<Layer>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Layer {
pub name: String,
pub patterns: Vec<String>,
}
impl Config {
pub fn defaults() -> Self {
Config {
source_dirs: vec!["src".to_string()],
output_file: "docs/architecture.md".to_string(),
layout: "dependency-graph".to_string(),
exclude_patterns: vec![
"**/target/**".to_string(),
"**/node_modules/**".to_string(),
"**/.git/**".to_string(),
"**/tests/**".to_string(),
],
layers: vec![],
}
}
pub fn to_toml(&self) -> String {
let source_dirs = self
.source_dirs
.iter()
.map(|d| format!("\"{}\"", d))
.collect::<Vec<_>>()
.join(", ");
let exclude_patterns = self
.exclude_patterns
.iter()
.map(|p| format!("\"{}\"", p))
.collect::<Vec<_>>()
.join(", ");
let mut toml = format!(
r#"# CodeTwin Configuration
# Code → Diagram/Documentation Generator
# https://github.com/carlosferreyra/codetwin
# Source directories to scan
source_dirs = [{}]
# Output file for generated documentation
# The parent directory is used as the output directory.
output_file = "{}"
# Layout: dependency-graph, folder_markdown, one_per_file, layered, readme-embedded
layout = "{}"
# Patterns to exclude from scanning
exclude_patterns = [{}]
"#,
source_dirs, self.output_file, self.layout, exclude_patterns
);
toml.push_str("\n# Optional: Define custom layers for layered layout\n");
toml.push_str("# If omitted, layers are auto-detected from directory structure\n");
toml.push_str("# Example configuration (uncomment and customize):\n");
toml.push_str("#\n");
toml.push_str("# [[layers]]\n");
toml.push_str("# name = \"Core\"\n");
toml.push_str("# patterns = [\"src/lib.rs\", \"src/core/ir.rs\"]\n");
toml.push_str("#\n");
toml.push_str("# [[layers]]\n");
toml.push_str("# name = \"Engine\"\n");
toml.push_str("# patterns = [\"src/app/engine.rs\", \"src/cli/mod.rs\"]\n");
toml.push_str("#\n");
toml.push_str("# [[layers]]\n");
toml.push_str("# name = \"Drivers\"\n");
toml.push_str("# patterns = [\"src/drivers/**\"]\n");
if !self.layers.is_empty() {
toml.push_str("\n# Layer configuration (for layered layout)\n");
for layer in &self.layers {
let patterns = layer
.patterns
.iter()
.map(|p| format!("\"{}\"", p))
.collect::<Vec<_>>()
.join(", ");
toml.push_str(&format!(
"\n[[layers]]\nname = \"{}\"\npatterns = [{}]\n",
layer.name, patterns
));
}
}
toml
}
pub fn save(&self, force: bool) -> Result<()> {
let path = Path::new("codetwin.toml");
if path.exists() && !force {
return Err(anyhow!(
"codetwin.toml already initialized. Use --force to overwrite."
));
}
let content = self.to_toml();
fs::write(path, content).context("Failed to write codetwin.toml")?;
Ok(())
}
pub fn load_or_defaults(path: &str) -> Self {
match Self::load(path) {
Ok(config) => config,
Err(_) => Self::defaults(),
}
}
pub fn load(path: &str) -> Result<Self> {
let content = fs::read_to_string(path).context(format!("Failed to read {}", path))?;
let table: Table = content
.parse()
.context(format!("Failed to parse {}", path))?;
let source_dirs = table
.get("source_dirs")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_else(|| vec!["src".to_string()]);
let output_file = table
.get("output_file")
.and_then(|v| v.as_str())
.unwrap_or("docs/architecture.md")
.to_string();
let layout = table
.get("layout")
.and_then(|v| v.as_str())
.unwrap_or("dependency-graph")
.to_string();
let exclude_patterns = table
.get("exclude_patterns")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_else(|| {
vec![
"**/target/**".to_string(),
"**/node_modules/**".to_string(),
"**/.git/**".to_string(),
"**/tests/**".to_string(),
]
});
let layers = table
.get("layers")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|item| item.as_table())
.map(|layer_table| {
let name = layer_table
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("Unknown")
.to_string();
let patterns = layer_table
.get("patterns")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
Layer { name, patterns }
})
.collect()
})
.unwrap_or_default();
Ok(Config {
source_dirs,
output_file,
layout,
exclude_patterns,
layers,
})
}
}