Skip to main content

callisto_cli/render/
mod.rs

1use std::io;
2
3use callisto_model::{
4    ComposePrBodyReport, InitReport, PublishAttemptResult, PublishPlan, PublishReport,
5    SnapshotReport, StatusReport, TagReport, ValidateReport, VersionReport,
6};
7
8pub mod attribution;
9pub mod diff;
10
11pub fn render_diagnostics<W: io::Write>(
12    diagnostics: &[callisto_model::Diagnostic],
13    w: &mut W,
14) -> io::Result<()> {
15    if !diagnostics.is_empty() {
16        writeln!(w, "\nDiagnostics:")?;
17        for d in diagnostics {
18            writeln!(w, "  [{:?}] {}", d.severity, d.message)?;
19        }
20    }
21    Ok(())
22}
23
24pub fn render_status<W: io::Write>(report: &StatusReport, w: &mut W) -> io::Result<()> {
25    writeln!(w, "Status (schema v{}):", report.schema_version)?;
26    for pkg in &report.packages {
27        writeln!(
28            w,
29            "  {} {} (pending: {:?})",
30            pkg.package.display_name(),
31            pkg.current_version.raw(),
32            pkg.pending_severity
33        )?;
34    }
35    render_diagnostics(&report.diagnostics, w)
36}
37
38pub fn render_version<W: io::Write>(report: &VersionReport, w: &mut W) -> io::Result<()> {
39    writeln!(w, "Version Plan (schema v{}):", report.schema_version)?;
40    for bump in &report.bumps {
41        writeln!(
42            w,
43            "  {} {} → {}",
44            bump.package.display_name(),
45            bump.from.raw(),
46            bump.to.raw()
47        )?;
48    }
49    render_diagnostics(&report.diagnostics, w)
50}
51
52pub fn render_publish<W: io::Write>(report: &PublishPlan, w: &mut W) -> io::Result<()> {
53    writeln!(w, "Publish Plan (schema v{}):", report.schema_version)?;
54    for rel in &report.releases {
55        writeln!(w, "  Tag: {} (sha: {})", rel.tag_name, rel.sha.as_str())?;
56    }
57    Ok(())
58}
59
60pub fn render_publish_report<W: io::Write>(report: &PublishReport, w: &mut W) -> io::Result<()> {
61    writeln!(w, "Publish Report (schema v{}):", report.schema_version)?;
62    for attempt in &report.attempts {
63        let status = match &attempt.result {
64            PublishAttemptResult::Published => "published".to_string(),
65            PublishAttemptResult::AlreadyPublished => "already published".to_string(),
66            PublishAttemptResult::Failed { error } => format!("FAILED: {error}"),
67        };
68        writeln!(
69            w,
70            "  {} {} — {}",
71            attempt.package.display_name(),
72            attempt.version.raw(),
73            status
74        )?;
75    }
76    render_diagnostics(&report.diagnostics, w)
77}
78
79pub fn render_snapshot<W: io::Write>(report: &SnapshotReport, w: &mut W) -> io::Result<()> {
80    writeln!(w, "Snapshot Tag: {}", report.snapshot_tag)?;
81    for bump in &report.bumps {
82        writeln!(
83            w,
84            "  {} {} → {}",
85            bump.package.display_name(),
86            bump.from.raw(),
87            bump.to.raw()
88        )?;
89    }
90    Ok(())
91}
92
93pub fn render_validate<W: io::Write>(report: &ValidateReport, w: &mut W) -> io::Result<()> {
94    if report.valid {
95        writeln!(w, "Validation passed.")?;
96    } else {
97        writeln!(w, "Validation failed with diagnostics:")?;
98        for diag in &report.diagnostics {
99            writeln!(w, "  [{:?}] {}", diag.severity, diag.message)?;
100        }
101    }
102    Ok(())
103}
104
105pub fn render_tag<W: io::Write>(report: &TagReport, dry_run: bool, w: &mut W) -> io::Result<()> {
106    if dry_run {
107        writeln!(w, "Would create tags:")?;
108    } else {
109        writeln!(w, "Created Tags:")?;
110    }
111    for tag in &report.created_tags {
112        writeln!(w, "  {} ({})", tag.tag_name, tag.sha.as_str())?;
113    }
114    Ok(())
115}
116
117pub fn render_compose_pr_body<W: io::Write>(
118    report: &ComposePrBodyReport,
119    w: &mut W,
120) -> io::Result<()> {
121    write!(w, "{}", report.pr_body)?;
122    Ok(())
123}
124
125pub fn render_init<W: io::Write>(report: &InitReport, w: &mut W) -> io::Result<()> {
126    if report.initialized {
127        writeln!(
128            w,
129            "Initialized callisto configuration at {}",
130            report.config_path.display()
131        )?;
132    } else if report.diff.new_ecosystems.is_empty() {
133        writeln!(
134            w,
135            "callisto configuration at {} is up to date; nothing to reconcile",
136            report.config_path.display()
137        )?;
138    } else {
139        let names: Vec<&str> = report
140            .diff
141            .new_ecosystems
142            .iter()
143            .map(|e| e.prefix())
144            .collect();
145        if report.diff.applied {
146            writeln!(
147                w,
148                "Reconciled {}: added newly-detected ecosystem(s) {}",
149                report.config_path.display(),
150                names.join(", ")
151            )?;
152        } else {
153            writeln!(
154                w,
155                "Drift detected in {}: newly-detected ecosystem(s) {} — re-run with --yes to apply",
156                report.config_path.display(),
157                names.join(", ")
158            )?;
159        }
160    }
161    Ok(())
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167    use callisto_model::{Ecosystem, PackageId, PublishAttempt, Version, VersionGrammar};
168
169    fn v1() -> Version {
170        Version::parse("1.0.0", VersionGrammar::SemVer).unwrap()
171    }
172
173    fn pkg(name: &str) -> PackageId {
174        PackageId::Prefixed {
175            ecosystem: Ecosystem::Cargo,
176            name: name.to_string(),
177        }
178    }
179
180    fn mixed_report() -> PublishReport {
181        PublishReport {
182            schema_version: 1,
183            attempts: vec![
184                PublishAttempt {
185                    package: pkg("crate-a"),
186                    version: v1(),
187                    result: PublishAttemptResult::Published,
188                },
189                PublishAttempt {
190                    package: pkg("crate-b"),
191                    version: v1(),
192                    result: PublishAttemptResult::AlreadyPublished,
193                },
194                PublishAttempt {
195                    package: pkg("crate-c"),
196                    version: v1(),
197                    result: PublishAttemptResult::Failed {
198                        error: "auth failed: bad token".to_string(),
199                    },
200                },
201            ],
202            diagnostics: vec![],
203        }
204    }
205
206    #[test]
207    fn render_publish_report_text_distinguishes_per_package_outcomes() {
208        let mut out = Vec::new();
209        render_publish_report(&mixed_report(), &mut out).unwrap();
210        let text = String::from_utf8(out).unwrap();
211
212        assert!(text.contains("crate-a") && text.contains("published"));
213        assert!(text.contains("crate-b") && text.contains("already published"));
214        assert!(text.contains("crate-c") && text.contains("FAILED: auth failed: bad token"));
215    }
216
217    #[test]
218    fn publish_report_json_distinguishes_per_package_outcomes() {
219        let json = serde_json::to_string(&mixed_report()).unwrap();
220
221        assert!(json.contains("\"status\":\"published\""));
222        assert!(json.contains("\"status\":\"alreadyPublished\""));
223        assert!(json.contains("\"status\":\"failed\"") && json.contains("auth failed: bad token"));
224    }
225}