use std::collections::HashMap;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::core::errors::{Result, TgaError};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Config {
#[serde(default)]
pub repositories: Vec<RepositoryConfig>,
#[serde(default)]
pub team: Option<TeamConfig>,
#[serde(default)]
pub output: Option<OutputConfig>,
#[serde(default)]
pub classification: Option<ClassificationConfig>,
#[serde(default)]
pub github: Option<GithubConfig>,
#[serde(default)]
pub jira: Option<JiraConfig>,
#[serde(default)]
pub version: Option<String>,
#[serde(default)]
pub profile: Option<String>,
#[serde(default)]
pub developer_aliases: HashMap<String, Vec<String>>,
#[serde(default)]
pub analysis: Option<AnalysisConfig>,
#[serde(default)]
pub cache: Option<CacheConfig>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AnalysisConfig {
#[serde(default)]
pub ml_categorization: Option<MlCategorizationConfig>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct MlCategorizationConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub model: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CacheConfig {
#[serde(default)]
pub directory: Option<PathBuf>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct RepositoryConfig {
pub path: PathBuf,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub branch: Option<String>,
#[serde(default)]
pub since_date: Option<String>,
#[serde(default)]
pub until_date: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TeamConfig {
#[serde(default)]
pub members: Vec<TeamMember>,
#[serde(default)]
pub aliases: HashMap<String, String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TeamMember {
pub name: String,
pub email: String,
#[serde(default)]
pub aliases: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct OutputConfig {
#[serde(default)]
pub format: Option<String>,
#[serde(default, alias = "output_path")]
pub directory: Option<PathBuf>,
#[serde(default)]
pub formats: Vec<String>,
#[serde(default)]
pub include_unclassified: bool,
#[serde(default)]
pub include_merges: bool,
#[serde(default)]
pub include_files: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ClassificationConfig {
#[serde(default)]
pub rules_file: Option<PathBuf>,
#[serde(default)]
pub use_llm: bool,
#[serde(default)]
pub llm_model: Option<String>,
#[serde(default = "default_confidence_threshold")]
pub confidence_threshold: f64,
}
fn default_confidence_threshold() -> f64 {
0.7
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct GithubConfig {
#[serde(default)]
pub token: Option<String>,
#[serde(default)]
pub org: Option<String>,
#[serde(default)]
pub repo: Option<String>,
#[serde(default)]
pub fetch_prs: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct JiraConfig {
#[serde(default)]
pub url: Option<String>,
#[serde(default)]
pub username: Option<String>,
#[serde(default)]
pub token: Option<String>,
#[serde(default)]
pub project_key: Option<String>,
}
pub fn expand_path(path: &Path) -> PathBuf {
let s = match path.to_str() {
Some(s) => s,
None => return path.to_path_buf(),
};
if let Some(rest) = s.strip_prefix("~/") {
if let Some(home) = std::env::var_os("HOME") {
return PathBuf::from(home).join(rest);
}
} else if s == "~" {
if let Some(home) = std::env::var_os("HOME") {
return PathBuf::from(home);
}
}
path.to_path_buf()
}
impl Config {
pub fn load(path: &Path) -> Result<Config> {
let resolved = expand_path(path);
tracing::debug!(path = %resolved.display(), "loading config");
let text = std::fs::read_to_string(&resolved)?;
let cfg: Config = serde_yaml::from_str(&text)?;
Ok(cfg)
}
pub fn resolved_aliases(&self) -> HashMap<String, Vec<String>> {
if !self.developer_aliases.is_empty() {
self.developer_aliases.clone()
} else if let Some(team) = &self.team {
team.members
.iter()
.map(|m| (m.name.clone(), m.aliases.clone()))
.collect()
} else {
HashMap::new()
}
}
pub fn validate(&self) -> Result<()> {
if self.repositories.is_empty() {
return Err(TgaError::ValidationError(
"at least one repository must be configured".into(),
));
}
for r in &self.repositories {
if r.path.as_os_str().is_empty() {
return Err(TgaError::ValidationError(
"repository.path must not be empty".into(),
));
}
}
Ok(())
}
}