use serde::{Deserialize, Serialize};
use crate::domain::release::Release;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum CompatRule {
SpecDigestValid,
RequireToolsDigest,
RequireGraphDigest,
NoToolsChange,
NoGraphChange,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CompatRuleSet {
pub rules: Vec<CompatRule>,
}
impl CompatRuleSet {
pub fn standard() -> Self {
Self {
rules: vec![
CompatRule::SpecDigestValid,
CompatRule::RequireToolsDigest,
CompatRule::RequireGraphDigest,
],
}
}
pub fn with_rule(mut self, rule: CompatRule) -> Self {
self.rules.push(rule);
self
}
}
pub struct PromoteContext<'a> {
pub candidate: &'a Release,
pub current: Option<&'a Release>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CompatViolation {
pub rule: CompatRule,
pub reason: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CompatVerdict {
pub violations: Vec<CompatViolation>,
}
impl CompatVerdict {
pub fn passed(&self) -> bool {
self.violations.is_empty()
}
}
pub fn evaluate_compat(rule_set: &CompatRuleSet, ctx: &PromoteContext) -> CompatVerdict {
let mut violations = Vec::new();
for rule in &rule_set.rules {
if let Some(v) = check_rule(rule, ctx) {
violations.push(v);
}
}
CompatVerdict { violations }
}
fn is_valid_hex_digest(s: &str) -> bool {
s.len() == 64
&& s.chars()
.all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase())
}
fn check_rule(rule: &CompatRule, ctx: &PromoteContext) -> Option<CompatViolation> {
match rule {
CompatRule::SpecDigestValid => {
if !is_valid_hex_digest(&ctx.candidate.spec_digest) {
Some(CompatViolation {
rule: rule.clone(),
reason: format!(
"spec_digest '{}' is not a valid 64-char lowercase hex string",
ctx.candidate.spec_digest,
),
})
} else {
None
}
}
CompatRule::RequireToolsDigest => {
if ctx.candidate.tools_digest.is_empty() {
Some(CompatViolation {
rule: rule.clone(),
reason: "tools_digest is empty".to_string(),
})
} else {
None
}
}
CompatRule::RequireGraphDigest => {
if ctx.candidate.graph_digest.is_empty() {
Some(CompatViolation {
rule: rule.clone(),
reason: "graph_digest is empty".to_string(),
})
} else {
None
}
}
CompatRule::NoToolsChange => {
if let Some(current) = ctx.current {
if ctx.candidate.tools_digest != current.tools_digest {
Some(CompatViolation {
rule: rule.clone(),
reason: format!(
"tools_digest changed: '{}' -> '{}'",
current.tools_digest, ctx.candidate.tools_digest,
),
})
} else {
None
}
} else {
None
}
}
CompatRule::NoGraphChange => {
if let Some(current) = ctx.current {
if ctx.candidate.graph_digest != current.graph_digest {
Some(CompatViolation {
rule: rule.clone(),
reason: format!(
"graph_digest changed: '{}' -> '{}'",
current.graph_digest, ctx.candidate.graph_digest,
),
})
} else {
None
}
} else {
None
}
}
}
}