use std::cmp::Reverse;
use std::path::{Path, PathBuf};
use anyhow::Result;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ScanSummarySnapshot {
pub files_analyzed: u64,
pub files_skipped: u64,
pub total_physical_lines: u64,
pub code_lines: u64,
pub comment_lines: u64,
pub blank_lines: u64,
#[serde(default)]
pub functions: u64,
#[serde(default)]
pub classes: u64,
#[serde(default)]
pub variables: u64,
#[serde(default)]
pub imports: u64,
#[serde(default)]
pub test_count: u64,
#[serde(default)]
pub coverage_lines_found: u64,
#[serde(default)]
pub coverage_lines_hit: u64,
#[serde(default)]
pub coverage_functions_found: u64,
#[serde(default)]
pub coverage_functions_hit: u64,
#[serde(default)]
pub coverage_branches_found: u64,
#[serde(default)]
pub coverage_branches_hit: u64,
}
impl From<&crate::SummaryTotals> for ScanSummarySnapshot {
fn from(t: &crate::SummaryTotals) -> Self {
Self {
files_analyzed: t.files_analyzed,
files_skipped: t.files_skipped,
total_physical_lines: t.total_physical_lines,
code_lines: t.code_lines,
comment_lines: t.comment_lines,
blank_lines: t.blank_lines,
functions: t.functions,
classes: t.classes,
variables: t.variables,
imports: t.imports,
test_count: t.test_count,
coverage_lines_found: t.coverage_lines_found,
coverage_lines_hit: t.coverage_lines_hit,
coverage_functions_found: t.coverage_functions_found,
coverage_functions_hit: t.coverage_functions_hit,
coverage_branches_found: t.coverage_branches_found,
coverage_branches_hit: t.coverage_branches_hit,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegistryEntry {
pub run_id: String,
pub timestamp_utc: DateTime<Utc>,
pub project_label: String,
pub input_roots: Vec<String>,
pub json_path: Option<PathBuf>,
pub html_path: Option<PathBuf>,
#[serde(default)]
pub pdf_path: Option<PathBuf>,
#[serde(default)]
pub csv_path: Option<PathBuf>,
#[serde(default)]
pub xlsx_path: Option<PathBuf>,
pub summary: ScanSummarySnapshot,
#[serde(default)]
pub git_branch: Option<String>,
#[serde(default)]
pub git_commit: Option<String>,
#[serde(default)]
pub git_commit_long: Option<String>,
#[serde(default)]
pub git_author: Option<String>,
#[serde(default)]
pub git_tags: Option<String>,
#[serde(default)]
pub git_nearest_tag: Option<String>,
#[serde(default)]
pub git_commit_date: Option<String>,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct WatchedDirsStore {
pub dirs: Vec<PathBuf>,
}
impl WatchedDirsStore {
#[must_use]
pub fn load(path: &Path) -> Self {
std::fs::read_to_string(path)
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default()
}
pub fn save(&self, path: &Path) -> Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(path, serde_json::to_string_pretty(self)?)?;
Ok(())
}
pub fn add(&mut self, dir: PathBuf) {
if !self.dirs.contains(&dir) {
self.dirs.push(dir);
}
}
pub fn remove(&mut self, dir: &Path) {
self.dirs.retain(|d| d != dir);
}
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct ScanRegistry {
pub entries: Vec<RegistryEntry>,
}
impl ScanRegistry {
#[must_use]
pub fn load(registry_path: &Path) -> Self {
std::fs::read_to_string(registry_path)
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default()
}
pub fn save(&self, registry_path: &Path) -> Result<()> {
if let Some(parent) = registry_path.parent() {
std::fs::create_dir_all(parent)?;
}
let json = serde_json::to_string_pretty(self)?;
std::fs::write(registry_path, json)?;
Ok(())
}
pub fn add_entry(&mut self, entry: RegistryEntry) {
self.entries.retain(|e| e.run_id != entry.run_id);
self.entries.push(entry);
self.entries.sort_by_key(|e| Reverse(e.timestamp_utc));
}
#[must_use]
pub fn entries_for_roots(&self, roots: &[String]) -> Vec<&RegistryEntry> {
self.entries
.iter()
.filter(|e| e.input_roots == roots)
.collect()
}
#[must_use]
pub fn find_by_run_id(&self, run_id: &str) -> Option<&RegistryEntry> {
self.entries.iter().find(|e| e.run_id == run_id)
}
pub fn prune_stale(&mut self) {
self.entries
.retain(|e| e.json_path.as_ref().is_none_or(|p| p.exists()));
}
}
const fn default_interval_hours() -> u32 {
24
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CleanupPolicy {
pub enabled: bool,
#[serde(default)]
pub max_age_days: Option<u32>,
#[serde(default)]
pub max_run_count: Option<u32>,
#[serde(default = "default_interval_hours")]
pub interval_hours: u32,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct CleanupPolicyStore {
pub policy: Option<CleanupPolicy>,
#[serde(default)]
pub last_run_at: Option<DateTime<Utc>>,
#[serde(default)]
pub last_run_deleted: Option<u32>,
}
impl CleanupPolicyStore {
#[must_use]
pub fn load(path: &Path) -> Self {
std::fs::read_to_string(path)
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default()
}
pub fn save(&self, path: &Path) -> Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(path, serde_json::to_string_pretty(self)?)?;
Ok(())
}
}