use super::error::{CapabilityError, ErrorCategory};
use std::future::Future;
use std::pin::Pin;
use std::time::Duration;
use crate::gates::validation::ValidationReport;
use crate::types::{Actor, EvidenceRef, Fact, Proposal, TraceLink, Validated};
pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
#[derive(Debug, Clone)]
pub struct PromotionContext {
pub approver: Actor,
pub evidence: Vec<EvidenceRef>,
pub trace: TraceLink,
}
impl PromotionContext {
pub fn new(approver: Actor, trace: TraceLink) -> Self {
Self {
approver,
evidence: Vec::new(),
trace,
}
}
pub fn with_evidence(mut self, evidence: Vec<EvidenceRef>) -> Self {
self.evidence = evidence;
self
}
pub fn with_evidence_ref(mut self, evidence: EvidenceRef) -> Self {
self.evidence.push(evidence);
self
}
}
#[derive(Debug, Clone)]
pub enum PromoterError {
ReportMismatch {
expected: String,
actual: String,
},
Unauthorized {
actor: String,
reason: String,
},
AlreadyPromoted {
proposal_id: String,
fact_id: String,
},
Unavailable {
message: String,
},
Timeout {
elapsed: Duration,
deadline: Duration,
},
StorageError {
message: String,
},
Internal {
message: String,
},
}
impl std::fmt::Display for PromoterError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ReportMismatch { expected, actual } => {
write!(
f,
"validation report mismatch: expected proposal '{}', got '{}'",
expected, actual
)
}
Self::Unauthorized { actor, reason } => {
write!(f, "actor '{}' not authorized: {}", actor, reason)
}
Self::AlreadyPromoted {
proposal_id,
fact_id,
} => {
write!(
f,
"proposal '{}' already promoted to fact '{}'",
proposal_id, fact_id
)
}
Self::Unavailable { message } => write!(f, "promoter unavailable: {}", message),
Self::Timeout { elapsed, deadline } => {
write!(
f,
"promotion timeout after {:?} (deadline: {:?})",
elapsed, deadline
)
}
Self::StorageError { message } => write!(f, "storage error: {}", message),
Self::Internal { message } => write!(f, "internal promoter error: {}", message),
}
}
}
impl std::error::Error for PromoterError {}
impl CapabilityError for PromoterError {
fn category(&self) -> ErrorCategory {
match self {
Self::ReportMismatch { .. } => ErrorCategory::InvalidInput,
Self::Unauthorized { .. } => ErrorCategory::Auth,
Self::AlreadyPromoted { .. } => ErrorCategory::Conflict,
Self::Unavailable { .. } => ErrorCategory::Unavailable,
Self::Timeout { .. } => ErrorCategory::Timeout,
Self::StorageError { .. } => ErrorCategory::Internal,
Self::Internal { .. } => ErrorCategory::Internal,
}
}
fn is_transient(&self) -> bool {
matches!(
self,
Self::Unavailable { .. } | Self::Timeout { .. } | Self::StorageError { .. }
)
}
fn is_retryable(&self) -> bool {
self.is_transient() || matches!(self, Self::Internal { .. })
}
fn retry_after(&self) -> Option<Duration> {
None
}
}
pub trait Promoter: Send + Sync {
type PromoteFut<'a>: Future<Output = Result<Fact, PromoterError>> + Send + 'a
where
Self: 'a;
fn promote<'a>(
&'a self,
proposal: Proposal<Validated>,
report: &'a ValidationReport,
context: &'a PromotionContext,
) -> Self::PromoteFut<'a>;
}
pub trait DynPromoter: Send + Sync {
fn promote<'a>(
&'a self,
proposal: Proposal<Validated>,
report: &'a ValidationReport,
context: &'a PromotionContext,
) -> BoxFuture<'a, Result<Fact, PromoterError>>;
}
impl<T: Promoter> DynPromoter for T {
fn promote<'a>(
&'a self,
proposal: Proposal<Validated>,
report: &'a ValidationReport,
context: &'a PromotionContext,
) -> BoxFuture<'a, Result<Fact, PromoterError>> {
Box::pin(Promoter::promote(self, proposal, report, context))
}
}