use std::fmt;
use serde::{Deserialize, Serialize};
use crate::kind::EffectKind;
use crate::verification::VerificationMode;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Capabilities {
pub kind: EffectKind,
pub remote_idempotency: bool,
pub verification: VerificationMode,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum UnknownPlan {
Verify,
Reexecute,
Escalate,
}
impl Capabilities {
pub const fn unknown_plan(&self) -> UnknownPlan {
if !matches!(self.verification, VerificationMode::None) {
UnknownPlan::Verify
} else if self.kind.is_naturally_idempotent() || self.remote_idempotency {
UnknownPlan::Reexecute
} else {
UnknownPlan::Escalate
}
}
pub const fn unknown_always_escalates(&self) -> bool {
matches!(self.unknown_plan(), UnknownPlan::Escalate)
}
}
#[derive(
Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
#[serde(rename_all = "snake_case")]
pub enum RiskLevel {
#[default]
Low,
Medium,
High,
Critical,
}
impl fmt::Display for RiskLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Low => "low",
Self::Medium => "medium",
Self::High => "high",
Self::Critical => "critical",
})
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct Requirements {
pub approval: bool,
pub verification: bool,
pub no_automatic_retry: bool,
}
impl Requirements {
#[must_use]
pub const fn and(self, other: Self) -> Self {
Self {
approval: self.approval || other.approval,
verification: self.verification || other.verification,
no_automatic_retry: self.no_automatic_retry || other.no_automatic_retry,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct Selector {
risk: Option<RiskLevel>,
kind: Option<EffectKind>,
}
impl Selector {
fn matches(self, risk: RiskLevel, kind: EffectKind) -> bool {
self.risk.is_none_or(|r| r == risk) && self.kind.is_none_or(|k| k == kind)
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct RiskPolicy {
rules: Vec<(Selector, Requirements)>,
}
impl RiskPolicy {
pub fn requirements(&self, risk: RiskLevel, kind: EffectKind) -> Requirements {
self.rules
.iter()
.filter(|(selector, _)| selector.matches(risk, kind))
.fold(Requirements::default(), |all, (_, rule)| all.and(*rule))
}
}
#[derive(Clone, Debug, Default)]
#[must_use]
pub struct PolicyBuilder {
rules: Vec<(Selector, Requirements)>,
}
impl PolicyBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn for_risk(self, risk: RiskLevel) -> Self {
self.rule(Some(risk), None)
}
pub fn for_kind(self, kind: EffectKind) -> Self {
self.rule(None, Some(kind))
}
pub fn for_risk_and_kind(self, risk: RiskLevel, kind: EffectKind) -> Self {
self.rule(Some(risk), Some(kind))
}
pub fn auto_execute(self) -> Self {
self
}
pub fn require_approval(self) -> Self {
self.require(|r| r.approval = true)
}
pub fn require_verification(self) -> Self {
self.require(|r| r.verification = true)
}
pub fn disable_automatic_retry(self) -> Self {
self.require(|r| r.no_automatic_retry = true)
}
pub fn build(self) -> RiskPolicy {
RiskPolicy { rules: self.rules }
}
fn rule(mut self, risk: Option<RiskLevel>, kind: Option<EffectKind>) -> Self {
self.rules
.push((Selector { risk, kind }, Requirements::default()));
self
}
fn require(mut self, add: impl FnOnce(&mut Requirements)) -> Self {
if self.rules.is_empty() {
self = self.rule(None, None);
}
if let Some((_, requirements)) = self.rules.last_mut() {
add(requirements);
}
self
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
const KINDS: [EffectKind; 4] = [
EffectKind::Read,
EffectKind::IdempotentWrite,
EffectKind::ReversibleWrite,
EffectKind::IrreversibleWrite,
];
const MODES: [VerificationMode; 3] = [
VerificationMode::None,
VerificationMode::Authoritative,
VerificationMode::EventuallyConsistent {
settle: Duration::from_secs(5),
},
];
fn all_capabilities() -> impl Iterator<Item = Capabilities> {
KINDS.into_iter().flat_map(|kind| {
[false, true]
.into_iter()
.flat_map(move |remote_idempotency| {
MODES.into_iter().map(move |verification| Capabilities {
kind,
remote_idempotency,
verification,
})
})
})
}
#[test]
fn non_idempotent_writes_never_blindly_reexecute() {
for caps in all_capabilities() {
if caps.unknown_plan() == UnknownPlan::Reexecute {
assert!(
caps.kind.is_naturally_idempotent() || caps.remote_idempotency,
"{caps:?} re-executes blindly"
);
}
}
}
#[test]
fn escalation_happens_only_without_any_safe_option() {
for caps in all_capabilities() {
let no_safe_option = caps.verification == VerificationMode::None
&& !caps.kind.is_naturally_idempotent()
&& !caps.remote_idempotency;
assert_eq!(caps.unknown_always_escalates(), no_safe_option, "{caps:?}");
}
}
const RISKS: [RiskLevel; 4] = [
RiskLevel::Low,
RiskLevel::Medium,
RiskLevel::High,
RiskLevel::Critical,
];
fn sample_policy() -> RiskPolicy {
PolicyBuilder::new()
.for_risk(RiskLevel::Low)
.auto_execute()
.for_risk(RiskLevel::Medium)
.require_verification()
.for_risk(RiskLevel::High)
.require_approval()
.for_risk(RiskLevel::Critical)
.require_approval()
.disable_automatic_retry()
.for_kind(EffectKind::IrreversibleWrite)
.require_verification()
.build()
}
#[test]
fn requirements_are_the_union_of_matching_rules() {
let policy = sample_policy();
let low_read = policy.requirements(RiskLevel::Low, EffectKind::Read);
assert_eq!(low_read, Requirements::default());
let high_irreversible = policy.requirements(RiskLevel::High, EffectKind::IrreversibleWrite);
assert!(high_irreversible.approval && high_irreversible.verification);
assert!(!high_irreversible.no_automatic_retry);
}
#[test]
fn rule_order_never_matters() {
let forward = sample_policy();
let mut reversed = forward.clone();
reversed.rules.reverse();
for risk in RISKS {
for kind in KINDS {
assert_eq!(
forward.requirements(risk, kind),
reversed.requirements(risk, kind)
);
}
}
}
#[test]
fn adding_a_rule_never_loosens() {
let base = sample_policy();
let mut extended = base.clone();
extended.rules.push((
Selector {
risk: None,
kind: None,
},
Requirements::default(),
));
for risk in RISKS {
for kind in KINDS {
let (before, after) = (
base.requirements(risk, kind),
extended.requirements(risk, kind),
);
assert_eq!(before.and(after), after, "{risk} {kind:?}");
}
}
}
#[test]
fn requirements_without_a_rule_apply_to_everything() {
let policy = PolicyBuilder::new().disable_automatic_retry().build();
for risk in RISKS {
assert!(
policy
.requirements(risk, EffectKind::Read)
.no_automatic_retry
);
}
}
}