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}