use std::fmt;
#[derive(Debug, Clone, PartialEq)]
pub enum PublisherState {
Clean,
Published,
InModeration { reason: String },
PRPending(String),
Unknown { reason: String },
}
impl PublisherState {
pub fn is_blocker(&self, strict: bool) -> bool {
match self {
PublisherState::InModeration { .. } | PublisherState::PRPending(_) => true,
PublisherState::Unknown { .. } => strict,
_ => false,
}
}
pub fn label(&self) -> &'static str {
match self {
PublisherState::Clean => "clean",
PublisherState::Published => "published",
PublisherState::InModeration { .. } => "in-moderation",
PublisherState::PRPending(_) => "pr-pending",
PublisherState::Unknown { .. } => "unknown",
}
}
}
impl fmt::Display for PublisherState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PublisherState::Clean => write!(f, "clean"),
PublisherState::Published => write!(f, "already published (idempotent skip)"),
PublisherState::InModeration { reason } => {
write!(f, "in moderation queue: {} — BLOCKER", reason)
}
PublisherState::PRPending(url) => write!(f, "PR already open: {} — BLOCKER", url),
PublisherState::Unknown { reason } => write!(f, "unknown ({})", reason),
}
}
}
#[derive(Debug, Clone)]
pub struct PreflightEntry {
pub publisher: String,
pub package: String,
pub version: String,
pub state: PublisherState,
}
#[derive(Debug, Default)]
pub struct PreflightReport {
pub entries: Vec<PreflightEntry>,
pub warnings: Vec<String>,
pub blockers: Vec<String>,
}
impl PreflightReport {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, entry: PreflightEntry) {
self.entries.push(entry);
}
pub fn clean_count(&self) -> usize {
self.entries
.iter()
.filter(|e| e.state == PublisherState::Clean)
.count()
}
pub fn has_blockers(&self, strict: bool) -> bool {
self.entries.iter().any(|e| e.state.is_blocker(strict))
}
pub fn blockers(&self, strict: bool) -> Vec<&PreflightEntry> {
self.entries
.iter()
.filter(|e| e.state.is_blocker(strict))
.collect()
}
}
impl fmt::Display for PreflightReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Pre-flight publisher check:")?;
for entry in &self.entries {
writeln!(
f,
" [{:>14}] {} {}@{}",
entry.state.label(),
entry.publisher,
entry.package,
entry.version
)?;
match &entry.state {
PublisherState::PRPending(url) => {
writeln!(f, " PR: {}", url)?;
}
PublisherState::Unknown { reason } | PublisherState::InModeration { reason } => {
writeln!(f, " reason: {}", reason)?;
}
_ => {}
}
}
for w in &self.warnings {
writeln!(f, " [ warning] {}", w)?;
}
for b in &self.blockers {
writeln!(f, " [ blocker] {}", b)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn entry(publisher: &str, state: PublisherState) -> PreflightEntry {
PreflightEntry {
publisher: publisher.to_string(),
package: "mypkg".to_string(),
version: "1.2.3".to_string(),
state,
}
}
#[test]
fn report_aggregation_four_publishers() {
let mut report = PreflightReport::new();
report.push(entry("cargo", PublisherState::Clean));
report.push(entry(
"chocolatey",
PublisherState::InModeration {
reason: "package in moderation queue".into(),
},
));
report.push(entry(
"winget",
PublisherState::PRPending("https://github.com/microsoft/winget-pkgs/pull/123".into()),
));
report.push(entry(
"aur",
PublisherState::Unknown {
reason: "AUR RPC returned 503".into(),
},
));
assert_eq!(report.clean_count(), 1);
assert!(report.has_blockers(false));
let blockers = report.blockers(false);
assert_eq!(blockers.len(), 2);
assert!(blockers.iter().any(|e| e.publisher == "chocolatey"));
assert!(blockers.iter().any(|e| e.publisher == "winget"));
assert!(report.has_blockers(true));
let strict_blockers = report.blockers(true);
assert_eq!(strict_blockers.len(), 3);
}
#[test]
fn report_all_clean_no_blockers() {
let mut report = PreflightReport::new();
report.push(entry("cargo", PublisherState::Clean));
report.push(entry("aur", PublisherState::Clean));
assert!(!report.has_blockers(false));
assert!(!report.has_blockers(true));
assert_eq!(report.clean_count(), 2);
}
#[test]
fn published_is_not_blocker() {
let mut report = PreflightReport::new();
report.push(entry("cargo", PublisherState::Published));
assert!(!report.has_blockers(false));
assert!(!report.has_blockers(true));
}
#[test]
fn unknown_only_blocks_when_strict() {
let mut report = PreflightReport::new();
report.push(entry(
"aur",
PublisherState::Unknown {
reason: "timeout".into(),
},
));
assert!(!report.has_blockers(false));
assert!(report.has_blockers(true));
}
#[test]
fn display_includes_blocker_label() {
let mut report = PreflightReport::new();
report.push(entry(
"chocolatey",
PublisherState::InModeration {
reason: "package in moderation queue".into(),
},
));
let s = report.to_string();
assert!(s.contains("in-moderation"), "display: {s}");
assert!(s.contains("chocolatey"), "display: {s}");
}
}