use std::{collections::HashMap, fs, path::Path};
use serde::{Deserialize, Serialize};
use crate::{
error::{CommitGenError, Result},
types::ConventionalAnalysis,
};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Manifest {
#[serde(default)]
pub fixtures: HashMap<String, FixtureEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FixtureEntry {
pub description: String,
#[serde(default)]
pub tags: Vec<String>,
}
impl Manifest {
pub fn load(fixtures_dir: &Path) -> Result<Self> {
let path = fixtures_dir.join("manifest.toml");
if !path.exists() {
return Ok(Self { fixtures: HashMap::new() });
}
let content = fs::read_to_string(&path)?;
toml::from_str(&content)
.map_err(|e| CommitGenError::Other(format!("Failed to parse manifest.toml: {e}")))
}
pub fn save(&self, fixtures_dir: &Path) -> Result<()> {
let path = fixtures_dir.join("manifest.toml");
let content = toml::to_string_pretty(self)
.map_err(|e| CommitGenError::Other(format!("Failed to serialize manifest: {e}")))?;
fs::write(&path, content)?;
Ok(())
}
pub fn add(&mut self, name: String, entry: FixtureEntry) {
self.fixtures.insert(name, entry);
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FixtureMeta {
pub source_repo: String,
pub source_commit: String,
pub description: String,
pub captured_at: String,
#[serde(default)]
pub tags: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct FixtureContext {
#[serde(default)]
pub recent_commits: Option<String>,
#[serde(default)]
pub common_scopes: Option<String>,
#[serde(default)]
pub project_context: Option<String>,
#[serde(default)]
pub user_context: Option<String>,
}
#[derive(Debug, Clone)]
pub struct FixtureInput {
pub diff: String,
pub stat: String,
pub scope_candidates: String,
pub context: FixtureContext,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Golden {
pub analysis: ConventionalAnalysis,
pub final_message: String,
}
#[derive(Debug, Clone)]
pub struct Fixture {
pub name: String,
pub meta: FixtureMeta,
pub input: FixtureInput,
pub golden: Option<Golden>,
}
impl Fixture {
pub fn load(fixtures_dir: &Path, name: &str) -> Result<Self> {
let fixture_dir = fixtures_dir.join(name);
if !fixture_dir.exists() {
return Err(CommitGenError::Other(format!(
"Fixture '{}' not found at {}",
name,
fixture_dir.display()
)));
}
let meta_path = fixture_dir.join("meta.toml");
let meta: FixtureMeta = if meta_path.exists() {
let content = fs::read_to_string(&meta_path)?;
toml::from_str(&content).map_err(|e| {
CommitGenError::Other(format!("Failed to parse {}: {e}", meta_path.display()))
})?
} else {
return Err(CommitGenError::Other(format!("Fixture '{name}' missing meta.toml")));
};
let input_dir = fixture_dir.join("input");
let diff = fs::read_to_string(input_dir.join("diff.patch"))
.map_err(|e| CommitGenError::Other(format!("Failed to read diff.patch: {e}")))?;
let stat = fs::read_to_string(input_dir.join("stat.txt"))
.map_err(|e| CommitGenError::Other(format!("Failed to read stat.txt: {e}")))?;
let scope_candidates =
fs::read_to_string(input_dir.join("scope_candidates.txt")).unwrap_or_default();
let context_path = input_dir.join("context.toml");
let context: FixtureContext = if context_path.exists() {
let content = fs::read_to_string(&context_path)?;
toml::from_str(&content)
.map_err(|e| CommitGenError::Other(format!("Failed to parse context.toml: {e}")))?
} else {
FixtureContext::default()
};
let golden_dir = fixture_dir.join("golden");
let golden = if golden_dir.exists() {
let analysis_path = golden_dir.join("analysis.json");
let final_path = golden_dir.join("final.txt");
if analysis_path.exists() && final_path.exists() {
let analysis_content = fs::read_to_string(&analysis_path)?;
let analysis: ConventionalAnalysis = serde_json::from_str(&analysis_content)
.map_err(|e| CommitGenError::Other(format!("Failed to parse analysis.json: {e}")))?;
let final_message = fs::read_to_string(&final_path)?;
Some(Golden { analysis, final_message })
} else {
None
}
} else {
None
};
Ok(Self {
name: name.to_string(),
meta,
input: FixtureInput { diff, stat, scope_candidates, context },
golden,
})
}
pub fn save(&self, fixtures_dir: &Path) -> Result<()> {
let fixture_dir = fixtures_dir.join(&self.name);
let input_dir = fixture_dir.join("input");
let golden_dir = fixture_dir.join("golden");
fs::create_dir_all(&input_dir)?;
fs::create_dir_all(&golden_dir)?;
let meta_content = toml::to_string_pretty(&self.meta)
.map_err(|e| CommitGenError::Other(format!("Failed to serialize meta: {e}")))?;
fs::write(fixture_dir.join("meta.toml"), meta_content)?;
fs::write(input_dir.join("diff.patch"), &self.input.diff)?;
fs::write(input_dir.join("stat.txt"), &self.input.stat)?;
fs::write(input_dir.join("scope_candidates.txt"), &self.input.scope_candidates)?;
let context_content = toml::to_string_pretty(&self.input.context)
.map_err(|e| CommitGenError::Other(format!("Failed to serialize context: {e}")))?;
fs::write(input_dir.join("context.toml"), context_content)?;
if let Some(golden) = &self.golden {
let analysis_json = serde_json::to_string_pretty(&golden.analysis)?;
fs::write(golden_dir.join("analysis.json"), analysis_json)?;
fs::write(golden_dir.join("final.txt"), &golden.final_message)?;
}
Ok(())
}
pub fn update_golden(&mut self, analysis: ConventionalAnalysis, final_message: String) {
self.golden = Some(Golden { analysis, final_message });
}
}
pub fn discover_fixtures(fixtures_dir: &Path) -> Result<Vec<String>> {
let mut fixtures = Vec::new();
if !fixtures_dir.exists() {
return Ok(fixtures);
}
for entry in fs::read_dir(fixtures_dir)? {
let entry = entry?;
let path = entry.path();
if !path.is_dir() {
continue;
}
if path.join("meta.toml").exists()
&& let Some(name) = path.file_name().and_then(|n| n.to_str())
{
fixtures.push(name.to_string());
}
}
fixtures.sort();
Ok(fixtures)
}