use chrono::Utc;
use serde::{Deserialize, Serialize};
use turbovault_core::prelude::*;
use turbovault_core::to_json_string;
#[derive(Debug, Clone, Copy)]
pub enum ExportFormat {
Json,
Csv,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthReport {
pub timestamp: String,
pub vault_name: String,
pub health_score: u8,
pub total_notes: usize,
pub total_links: usize,
pub broken_links: usize,
pub orphaned_notes: usize,
pub connectivity_rate: f64,
pub link_density: f64,
pub status: String,
pub recommendations: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BrokenLinkRecord {
pub source_file: String,
pub target: String,
pub line: usize,
pub suggestions: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VaultStatsRecord {
pub timestamp: String,
pub vault_name: String,
pub total_files: usize,
pub total_links: usize,
pub orphaned_files: usize,
pub average_links_per_file: f64,
#[serde(default)]
pub total_words: usize,
#[serde(default)]
pub total_readable_chars: usize,
#[serde(default)]
pub avg_words_per_note: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnalysisReport {
pub timestamp: String,
pub vault_name: String,
pub health: HealthReport,
pub broken_links_count: usize,
pub orphaned_notes_count: usize,
pub recommendations: Vec<String>,
}
pub struct HealthReportExporter;
impl HealthReportExporter {
pub fn to_json(report: &HealthReport) -> Result<String> {
to_json_string(report, "health report")
}
pub fn to_csv(report: &HealthReport) -> Result<String> {
let csv = format!(
"timestamp,vault_name,health_score,total_notes,total_links,broken_links,orphaned_notes,connectivity_rate,link_density,status\n\
{},{},{},{},{},{},{},{:.3},{:.3},{}",
report.timestamp,
report.vault_name,
report.health_score,
report.total_notes,
report.total_links,
report.broken_links,
report.orphaned_notes,
report.connectivity_rate,
report.link_density,
report.status
);
Ok(csv)
}
}
pub struct BrokenLinksExporter;
impl BrokenLinksExporter {
pub fn to_json(links: &[BrokenLinkRecord]) -> Result<String> {
to_json_string(links, "broken links")
}
pub fn to_csv(links: &[BrokenLinkRecord]) -> Result<String> {
let mut csv = String::from("source_file,target,line,suggestions\n");
for link in links {
let suggestions = link.suggestions.join("|");
csv.push_str(&format!(
"\"{}\",\"{}\",{},\"{}\"\n",
link.source_file, link.target, link.line, suggestions
));
}
Ok(csv)
}
}
pub struct VaultStatsExporter;
impl VaultStatsExporter {
pub fn to_json(stats: &VaultStatsRecord) -> Result<String> {
to_json_string(stats, "vault stats")
}
pub fn to_csv(stats: &VaultStatsRecord) -> Result<String> {
let csv = format!(
"timestamp,vault_name,total_files,total_links,orphaned_files,average_links_per_file,total_words,total_readable_chars,avg_words_per_note\n\
{},{},{},{},{},{:.3},{},{},{:.1}",
stats.timestamp,
stats.vault_name,
stats.total_files,
stats.total_links,
stats.orphaned_files,
stats.average_links_per_file,
stats.total_words,
stats.total_readable_chars,
stats.avg_words_per_note
);
Ok(csv)
}
}
pub struct AnalysisReportExporter;
impl AnalysisReportExporter {
pub fn to_json(report: &AnalysisReport) -> Result<String> {
to_json_string(report, "analysis report")
}
pub fn to_csv(report: &AnalysisReport) -> Result<String> {
let csv = format!(
"timestamp,vault_name,health_score,total_notes,total_links,broken_links,orphaned_notes,broken_links_count,recommendations\n\
{},{},{},{},{},{},{},{},\"{}\"",
report.timestamp,
report.vault_name,
report.health.health_score,
report.health.total_notes,
report.health.total_links,
report.health.broken_links,
report.health.orphaned_notes,
report.broken_links_count,
report.recommendations.join("|")
);
Ok(csv)
}
}
pub fn create_health_report(
vault_name: &str,
health_score: u8,
total_notes: usize,
total_links: usize,
broken_links: usize,
orphaned_notes: usize,
) -> HealthReport {
let connectivity_rate = if total_notes > 0 {
(total_notes - orphaned_notes) as f64 / total_notes as f64
} else {
0.0
};
let link_density = if total_notes > 1 {
total_links as f64 / ((total_notes as f64) * (total_notes as f64 - 1.0))
} else {
0.0
};
let status = if health_score >= 80 {
"Healthy".to_string()
} else if health_score >= 60 {
"Fair".to_string()
} else if health_score >= 40 {
"Needs Attention".to_string()
} else {
"Critical".to_string()
};
let mut recommendations = Vec::new();
if broken_links > 0 {
recommendations.push(format!(
"Found {} broken links. Consider fixing or updating them.",
broken_links
));
}
if orphaned_notes as f64 / total_notes as f64 > 0.1 {
recommendations
.push("Over 10% of notes are orphaned. Link them to improve connectivity.".to_string());
}
if link_density < 0.05 {
recommendations.push(
"Low link density. Consider adding more cross-references between notes.".to_string(),
);
}
HealthReport {
timestamp: Utc::now().to_rfc3339(),
vault_name: vault_name.to_string(),
health_score,
total_notes,
total_links,
broken_links,
orphaned_notes,
connectivity_rate,
link_density,
status,
recommendations,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_health_report_creation() {
let report = create_health_report("test", 85, 100, 150, 2, 5);
assert_eq!(report.vault_name, "test");
assert_eq!(report.health_score, 85);
assert_eq!(report.status, "Healthy");
}
#[test]
fn test_health_report_json_export() {
let report = create_health_report("test", 85, 100, 150, 2, 5);
let json = HealthReportExporter::to_json(&report).unwrap();
assert!(json.contains("test"));
assert!(json.contains("85"));
}
#[test]
fn test_health_report_csv_export() {
let report = create_health_report("test", 85, 100, 150, 2, 5);
let csv = HealthReportExporter::to_csv(&report).unwrap();
assert!(csv.contains("test"));
assert!(csv.contains("85"));
}
#[test]
fn test_broken_links_export() {
let links = vec![BrokenLinkRecord {
source_file: "file.md".to_string(),
target: "missing.md".to_string(),
line: 5,
suggestions: vec!["existing.md".to_string()],
}];
let json = BrokenLinksExporter::to_json(&links).unwrap();
assert!(json.contains("file.md"));
assert!(json.contains("missing.md"));
let csv = BrokenLinksExporter::to_csv(&links).unwrap();
assert!(csv.contains("file.md"));
}
#[test]
fn test_vault_stats_export() {
let stats = VaultStatsRecord {
timestamp: "2025-01-01T00:00:00Z".to_string(),
vault_name: "test".to_string(),
total_files: 100,
total_links: 150,
orphaned_files: 5,
average_links_per_file: 1.5,
total_words: 25000,
total_readable_chars: 150000,
avg_words_per_note: 250.0,
};
let json = VaultStatsExporter::to_json(&stats).unwrap();
assert!(json.contains("100"));
assert!(json.contains("25000"));
let csv = VaultStatsExporter::to_csv(&stats).unwrap();
assert!(csv.contains("100"));
assert!(csv.contains("25000"));
}
}