Skip to main content

lexicon_api/
report.rs

1use crate::diff::{ApiChange, ApiDiff, BreakingLevel, ChangeDetail, classify_change};
2use crate::error::ApiError;
3
4/// Format a human-readable diff report.
5pub fn format_diff_report(diff: &ApiDiff) -> String {
6    if diff.is_empty() {
7        return "No API changes detected.".into();
8    }
9
10    let mut report = String::new();
11    report.push_str(&format!("API Diff Summary: {}\n", diff.summary()));
12    report.push_str(&"=".repeat(60));
13    report.push('\n');
14
15    // Group changes by breaking level
16    if !diff.removed.is_empty() {
17        report.push_str("\n[BREAKING] Removed items:\n");
18        for item in &diff.removed {
19            report.push_str(&format!(
20                "  - {} {} ({})\n",
21                item.kind, item.name, item.visibility
22            ));
23            if !item.module_path.is_empty() {
24                report.push_str(&format!("    path: {}\n", item.module_path.join("::")));
25            }
26        }
27    }
28
29    // Separate changed items by breaking level
30    let breaking_changes: Vec<&ApiChange> = diff.changed.iter()
31        .filter(|c| classify_change(c) == BreakingLevel::Breaking)
32        .collect();
33    let dangerous_changes: Vec<&ApiChange> = diff.changed.iter()
34        .filter(|c| classify_change(c) == BreakingLevel::Dangerous)
35        .collect();
36    let additive_changes: Vec<&ApiChange> = diff.changed.iter()
37        .filter(|c| classify_change(c) == BreakingLevel::Additive)
38        .collect();
39
40    if !breaking_changes.is_empty() {
41        report.push_str("\n[BREAKING] Changed items:\n");
42        for change in &breaking_changes {
43            format_change(&mut report, change);
44        }
45    }
46
47    if !dangerous_changes.is_empty() {
48        report.push_str("\n[DANGEROUS] Changed items:\n");
49        for change in &dangerous_changes {
50            format_change(&mut report, change);
51        }
52    }
53
54    if !additive_changes.is_empty() {
55        report.push_str("\n[ADDITIVE] Changed items:\n");
56        for change in &additive_changes {
57            format_change(&mut report, change);
58        }
59    }
60
61    if !diff.added.is_empty() {
62        report.push_str("\n[ADDITIVE] Added items:\n");
63        for item in &diff.added {
64            report.push_str(&format!(
65                "  + {} {} ({})\n",
66                item.kind, item.name, item.visibility
67            ));
68            if !item.module_path.is_empty() {
69                report.push_str(&format!("    path: {}\n", item.module_path.join("::")));
70            }
71        }
72    }
73
74    report
75}
76
77fn format_change(report: &mut String, change: &ApiChange) {
78    report.push_str(&format!("  ~ {} {}\n", change.kind, change.name));
79    if !change.module_path.is_empty() {
80        report.push_str(&format!("    path: {}\n", change.module_path.join("::")));
81    }
82    for detail in &change.changes {
83        match detail {
84            ChangeDetail::SignatureChanged { old, new } => {
85                report.push_str(&format!("    signature: {old} -> {new}\n"));
86            }
87            ChangeDetail::VisibilityChanged { old, new } => {
88                report.push_str(&format!("    visibility: {old} -> {new}\n"));
89            }
90        }
91    }
92}
93
94/// Format a JSON diff report.
95pub fn format_json_report(diff: &ApiDiff) -> Result<String, ApiError> {
96    let json = serde_json::to_string_pretty(diff)?;
97    Ok(json)
98}