pub mod denial;
pub mod receipt;
pub use denial::{Denial, DenialPayload, GateEvaluation, GateId, GateIdError, Verdict};
pub use receipt::Receipt;
pub trait Gate<Ctx>: Send + Sync {
fn name(&self) -> &'static str;
fn evaluate(&self, ctx: &Ctx) -> Result<(), Denial>;
fn description(&self) -> &'static str {
""
}
}
pub struct GateSet<Ctx> {
gates: Vec<Box<dyn Gate<Ctx>>>,
}
impl<Ctx> GateSet<Ctx> {
pub fn new() -> Self {
Self { gates: vec![] }
}
pub fn push(&mut self, gate: impl Gate<Ctx> + 'static) {
self.gates.push(Box::new(gate));
}
pub fn evaluate<T>(
&self,
ctx: &Ctx,
proposal: crate::pipeline::Proposal<T>,
) -> Result<Receipt<T>, Denial> {
for gate in &self.gates {
gate.evaluate(ctx)?;
}
let names: Vec<&'static str> = self.gates.iter().map(|g| g.name()).collect();
Ok(Receipt::new(proposal.0, names))
}
pub fn evaluate_all(&self, ctx: &Ctx) -> Vec<Denial> {
self.gates
.iter()
.filter_map(|g| {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| g.evaluate(ctx))) {
Ok(Ok(())) => None,
Ok(Err(denial)) => Some(denial),
Err(panic_payload) => {
let msg = if let Some(s) = panic_payload.downcast_ref::<&str>() {
(*s).to_string()
} else if let Some(s) = panic_payload.downcast_ref::<String>() {
s.clone()
} else {
"unknown panic".to_string()
};
Some(Denial::new(g.name(), msg).with_code("GATE_DEFECT"))
}
}
})
.collect()
}
pub fn trace_denial(
&self,
failing: &Denial,
proposed_kind: crate::event::EventKind,
proposed_content_hash: Option<[u8; 32]>,
pipeline_id: Option<String>,
) -> DenialPayload {
let mut matched_denier = false;
let mut evaluations = Vec::with_capacity(self.gates.len().max(1));
for gate in &self.gates {
let gate_id = GateId::from_name(gate.name());
let verdict = if matched_denier {
Verdict::Skipped
} else if gate.name() == failing.gate {
matched_denier = true;
Verdict::Deny {
code: failing.code.clone(),
message: failing.message.clone(),
context: failing.context.clone(),
}
} else {
Verdict::Permit
};
evaluations.push(GateEvaluation::new(gate_id, verdict, None));
}
if !matched_denier {
evaluations.push(GateEvaluation::new(
GateId::from_name(failing.gate),
Verdict::Deny {
code: failing.code.clone(),
message: failing.message.clone(),
context: failing.context.clone(),
},
None,
));
}
DenialPayload::new(
evaluations,
pipeline_id,
proposed_kind,
proposed_content_hash,
)
}
pub fn len(&self) -> usize {
self.gates.len()
}
pub fn is_empty(&self) -> bool {
self.gates.is_empty()
}
pub fn names(&self) -> Vec<&'static str> {
self.gates.iter().map(|g| g.name()).collect()
}
}
impl<Ctx> Default for GateSet<Ctx> {
fn default() -> Self {
Self::new()
}
}