use std::io;
use callisto_model::{
ComposePrBodyReport, InitReport, PublishAttemptResult, PublishPlan, PublishReport,
SnapshotReport, StatusReport, TagReport, ValidateReport, VersionReport,
};
pub mod attribution;
pub mod diff;
pub fn render_diagnostics<W: io::Write>(
diagnostics: &[callisto_model::Diagnostic],
w: &mut W,
) -> io::Result<()> {
if !diagnostics.is_empty() {
writeln!(w, "\nDiagnostics:")?;
for d in diagnostics {
writeln!(w, " [{:?}] {}", d.severity, d.message)?;
}
}
Ok(())
}
pub fn render_status<W: io::Write>(report: &StatusReport, w: &mut W) -> io::Result<()> {
writeln!(w, "Status (schema v{}):", report.schema_version)?;
for pkg in &report.packages {
writeln!(
w,
" {} {} (pending: {:?})",
pkg.package.display_name(),
pkg.current_version.raw(),
pkg.pending_severity
)?;
}
render_diagnostics(&report.diagnostics, w)
}
pub fn render_version<W: io::Write>(report: &VersionReport, w: &mut W) -> io::Result<()> {
writeln!(w, "Version Plan (schema v{}):", report.schema_version)?;
for bump in &report.bumps {
writeln!(
w,
" {} {} → {}",
bump.package.display_name(),
bump.from.raw(),
bump.to.raw()
)?;
}
render_diagnostics(&report.diagnostics, w)
}
pub fn render_publish<W: io::Write>(report: &PublishPlan, w: &mut W) -> io::Result<()> {
writeln!(w, "Publish Plan (schema v{}):", report.schema_version)?;
for rel in &report.releases {
writeln!(w, " Tag: {} (sha: {})", rel.tag_name, rel.sha.as_str())?;
}
Ok(())
}
pub fn render_publish_report<W: io::Write>(report: &PublishReport, w: &mut W) -> io::Result<()> {
writeln!(w, "Publish Report (schema v{}):", report.schema_version)?;
for attempt in &report.attempts {
let status = match &attempt.result {
PublishAttemptResult::Published => "published".to_string(),
PublishAttemptResult::AlreadyPublished => "already published".to_string(),
PublishAttemptResult::Failed { error } => format!("FAILED: {error}"),
};
writeln!(
w,
" {} {} — {}",
attempt.package.display_name(),
attempt.version.raw(),
status
)?;
}
render_diagnostics(&report.diagnostics, w)
}
pub fn render_snapshot<W: io::Write>(report: &SnapshotReport, w: &mut W) -> io::Result<()> {
writeln!(w, "Snapshot Tag: {}", report.snapshot_tag)?;
for bump in &report.bumps {
writeln!(
w,
" {} {} → {}",
bump.package.display_name(),
bump.from.raw(),
bump.to.raw()
)?;
}
Ok(())
}
pub fn render_validate<W: io::Write>(report: &ValidateReport, w: &mut W) -> io::Result<()> {
if report.valid {
writeln!(w, "Validation passed.")?;
} else {
writeln!(w, "Validation failed with diagnostics:")?;
for diag in &report.diagnostics {
writeln!(w, " [{:?}] {}", diag.severity, diag.message)?;
}
}
Ok(())
}
pub fn render_tag<W: io::Write>(report: &TagReport, dry_run: bool, w: &mut W) -> io::Result<()> {
if dry_run {
writeln!(w, "Would create tags:")?;
} else {
writeln!(w, "Created Tags:")?;
}
for tag in &report.created_tags {
writeln!(w, " {} ({})", tag.tag_name, tag.sha.as_str())?;
}
Ok(())
}
pub fn render_compose_pr_body<W: io::Write>(
report: &ComposePrBodyReport,
w: &mut W,
) -> io::Result<()> {
write!(w, "{}", report.pr_body)?;
Ok(())
}
pub fn render_init<W: io::Write>(report: &InitReport, w: &mut W) -> io::Result<()> {
if report.initialized {
writeln!(
w,
"Initialized callisto configuration at {}",
report.config_path.display()
)?;
} else if report.diff.new_ecosystems.is_empty() {
writeln!(
w,
"callisto configuration at {} is up to date; nothing to reconcile",
report.config_path.display()
)?;
} else {
let names: Vec<&str> = report
.diff
.new_ecosystems
.iter()
.map(|e| e.prefix())
.collect();
if report.diff.applied {
writeln!(
w,
"Reconciled {}: added newly-detected ecosystem(s) {}",
report.config_path.display(),
names.join(", ")
)?;
} else {
writeln!(
w,
"Drift detected in {}: newly-detected ecosystem(s) {} — re-run with --yes to apply",
report.config_path.display(),
names.join(", ")
)?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use callisto_model::{Ecosystem, PackageId, PublishAttempt, Version, VersionGrammar};
fn v1() -> Version {
Version::parse("1.0.0", VersionGrammar::SemVer).unwrap()
}
fn pkg(name: &str) -> PackageId {
PackageId::Prefixed {
ecosystem: Ecosystem::Cargo,
name: name.to_string(),
}
}
fn mixed_report() -> PublishReport {
PublishReport {
schema_version: 1,
attempts: vec![
PublishAttempt {
package: pkg("crate-a"),
version: v1(),
result: PublishAttemptResult::Published,
},
PublishAttempt {
package: pkg("crate-b"),
version: v1(),
result: PublishAttemptResult::AlreadyPublished,
},
PublishAttempt {
package: pkg("crate-c"),
version: v1(),
result: PublishAttemptResult::Failed {
error: "auth failed: bad token".to_string(),
},
},
],
diagnostics: vec![],
}
}
#[test]
fn render_publish_report_text_distinguishes_per_package_outcomes() {
let mut out = Vec::new();
render_publish_report(&mixed_report(), &mut out).unwrap();
let text = String::from_utf8(out).unwrap();
assert!(text.contains("crate-a") && text.contains("published"));
assert!(text.contains("crate-b") && text.contains("already published"));
assert!(text.contains("crate-c") && text.contains("FAILED: auth failed: bad token"));
}
#[test]
fn publish_report_json_distinguishes_per_package_outcomes() {
let json = serde_json::to_string(&mixed_report()).unwrap();
assert!(json.contains("\"status\":\"published\""));
assert!(json.contains("\"status\":\"alreadyPublished\""));
assert!(json.contains("\"status\":\"failed\"") && json.contains("auth failed: bad token"));
}
}