use std::process::ExitCode;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
Info,
Warn,
Error,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Finding {
pub rule: String,
pub message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub file: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub line: Option<u32>,
pub severity: Severity,
}
#[derive(Debug, Clone)]
pub enum Verdict {
Pass,
Warn {
findings: Vec<Finding>,
message: Option<String>,
},
Fail {
findings: Vec<Finding>,
message: String,
},
}
impl Verdict {
pub fn is_blocking(&self) -> bool {
matches!(self, Verdict::Fail { .. })
}
pub fn exit_code(&self) -> ExitCode {
if self.is_blocking() {
ExitCode::from(2)
} else {
ExitCode::SUCCESS
}
}
pub fn merge(verdicts: Vec<Verdict>, policy: VerdictPolicy) -> Verdict {
match policy {
VerdictPolicy::AnyFail => merge_any_fail(verdicts),
VerdictPolicy::AllFail => merge_all_fail(verdicts),
VerdictPolicy::MajorityFail => merge_majority_fail(verdicts),
}
}
}
fn merge_any_fail(verdicts: Vec<Verdict>) -> Verdict {
let mut fail_findings: Vec<Finding> = Vec::new();
let mut fail_messages: Vec<String> = Vec::new();
let mut warn_findings: Vec<Finding> = Vec::new();
let mut warn_messages: Vec<String> = Vec::new();
for verdict in verdicts {
match verdict {
Verdict::Pass => {}
Verdict::Warn { findings, message } => {
warn_findings.extend(findings);
if let Some(m) = message {
warn_messages.push(m);
}
}
Verdict::Fail { findings, message } => {
fail_findings.extend(findings);
fail_messages.push(message);
}
}
}
if !fail_messages.is_empty() {
Verdict::Fail {
findings: fail_findings,
message: fail_messages.join("\n"),
}
} else if !warn_findings.is_empty() || !warn_messages.is_empty() {
Verdict::Warn {
findings: warn_findings,
message: if warn_messages.is_empty() {
None
} else {
Some(warn_messages.join("\n"))
},
}
} else {
Verdict::Pass
}
}
struct Partition {
fail_count: usize,
pass_count: usize,
fail_findings: Vec<Finding>,
fail_messages: Vec<String>,
warn_findings: Vec<Finding>,
warn_messages: Vec<String>,
}
fn partition(verdicts: Vec<Verdict>) -> Partition {
let mut p = Partition {
fail_count: 0,
pass_count: 0,
fail_findings: Vec::new(),
fail_messages: Vec::new(),
warn_findings: Vec::new(),
warn_messages: Vec::new(),
};
for v in verdicts {
match v {
Verdict::Pass => p.pass_count += 1,
Verdict::Warn { findings, message } => {
p.warn_findings.extend(findings);
if let Some(m) = message {
p.warn_messages.push(m);
}
}
Verdict::Fail { findings, message } => {
p.fail_count += 1;
p.fail_findings.extend(findings);
p.fail_messages.push(message);
}
}
}
p
}
fn downgrade_to_warn_or_pass(p: Partition) -> Verdict {
if !p.fail_messages.is_empty() {
let mut findings = p.fail_findings;
findings.extend(p.warn_findings);
let mut messages = p.fail_messages;
messages.extend(p.warn_messages);
Verdict::Warn {
findings,
message: Some(messages.join("\n")),
}
} else if !p.warn_findings.is_empty() || !p.warn_messages.is_empty() {
Verdict::Warn {
findings: p.warn_findings,
message: if p.warn_messages.is_empty() {
None
} else {
Some(p.warn_messages.join("\n"))
},
}
} else {
Verdict::Pass
}
}
fn merge_all_fail(verdicts: Vec<Verdict>) -> Verdict {
let p = partition(verdicts);
if p.fail_count > 0 && p.pass_count == 0 {
Verdict::Fail {
findings: p.fail_findings,
message: p.fail_messages.join("\n"),
}
} else {
downgrade_to_warn_or_pass(p)
}
}
fn merge_majority_fail(verdicts: Vec<Verdict>) -> Verdict {
let p = partition(verdicts);
let total_decisive = p.fail_count + p.pass_count;
if total_decisive > 0 && p.fail_count * 2 > total_decisive {
Verdict::Fail {
findings: p.fail_findings,
message: p.fail_messages.join("\n"),
}
} else {
downgrade_to_warn_or_pass(p)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum VerdictPolicy {
#[default]
AnyFail,
AllFail,
MajorityFail,
}
#[cfg(test)]
#[path = "verdict_tests.rs"]
mod tests;