use super::error::{CapabilityError, ErrorCategory};
use std::future::Future;
use std::pin::Pin;
use std::time::Duration;
use crate::gates::validation::{ValidationPolicy, ValidationReport};
use crate::types::{Draft, Proposal};
pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
#[derive(Debug, Clone)]
pub enum ValidatorError {
CheckFailed {
check_name: String,
reason: String,
},
PolicyViolation {
policy: String,
message: String,
},
MissingEvidence {
expected: String,
},
Unavailable {
message: String,
},
Timeout {
elapsed: Duration,
deadline: Duration,
},
Internal {
message: String,
},
}
impl std::fmt::Display for ValidatorError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::CheckFailed { check_name, reason } => {
write!(f, "validation check '{}' failed: {}", check_name, reason)
}
Self::PolicyViolation { policy, message } => {
write!(f, "policy '{}' violated: {}", policy, message)
}
Self::MissingEvidence { expected } => {
write!(f, "missing required evidence: {}", expected)
}
Self::Unavailable { message } => write!(f, "validator unavailable: {}", message),
Self::Timeout { elapsed, deadline } => {
write!(
f,
"validation timeout after {:?} (deadline: {:?})",
elapsed, deadline
)
}
Self::Internal { message } => write!(f, "internal validator error: {}", message),
}
}
}
impl std::error::Error for ValidatorError {}
impl CapabilityError for ValidatorError {
fn category(&self) -> ErrorCategory {
match self {
Self::CheckFailed { .. } => ErrorCategory::InvalidInput,
Self::PolicyViolation { .. } => ErrorCategory::InvalidInput,
Self::MissingEvidence { .. } => ErrorCategory::InvalidInput,
Self::Unavailable { .. } => ErrorCategory::Unavailable,
Self::Timeout { .. } => ErrorCategory::Timeout,
Self::Internal { .. } => ErrorCategory::Internal,
}
}
fn is_transient(&self) -> bool {
matches!(self, Self::Unavailable { .. } | Self::Timeout { .. })
}
fn is_retryable(&self) -> bool {
self.is_transient() || matches!(self, Self::Internal { .. })
}
fn retry_after(&self) -> Option<Duration> {
None
}
}
pub trait Validator: Send + Sync {
type ValidateFut<'a>: Future<Output = Result<ValidationReport, ValidatorError>> + Send + 'a
where
Self: 'a;
fn validate<'a>(
&'a self,
proposal: &'a Proposal<Draft>,
policy: &'a ValidationPolicy,
) -> Self::ValidateFut<'a>;
}
pub trait DynValidator: Send + Sync {
fn validate<'a>(
&'a self,
proposal: &'a Proposal<Draft>,
policy: &'a ValidationPolicy,
) -> BoxFuture<'a, Result<ValidationReport, ValidatorError>>;
}
impl<T: Validator> DynValidator for T {
fn validate<'a>(
&'a self,
proposal: &'a Proposal<Draft>,
policy: &'a ValidationPolicy,
) -> BoxFuture<'a, Result<ValidationReport, ValidatorError>> {
Box::pin(Validator::validate(self, proposal, policy))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::error::{CapabilityError, ErrorCategory};
#[test]
fn display_check_failed() {
let e = ValidatorError::CheckFailed {
check_name: "schema".into(),
reason: "missing required field".into(),
};
let s = e.to_string();
assert!(s.contains("schema"));
assert!(s.contains("missing required field"));
}
#[test]
fn display_policy_violation() {
let e = ValidatorError::PolicyViolation {
policy: "no-pii".into(),
message: "SSN detected".into(),
};
let s = e.to_string();
assert!(s.contains("no-pii"));
assert!(s.contains("SSN detected"));
}
#[test]
fn display_missing_evidence() {
let e = ValidatorError::MissingEvidence {
expected: "receipt attachment".into(),
};
assert!(e.to_string().contains("receipt attachment"));
}
#[test]
fn display_unavailable() {
let e = ValidatorError::Unavailable {
message: "connection refused".into(),
};
assert!(e.to_string().contains("connection refused"));
}
#[test]
fn display_timeout() {
let e = ValidatorError::Timeout {
elapsed: Duration::from_secs(5),
deadline: Duration::from_secs(3),
};
let s = e.to_string();
assert!(s.contains("5s"));
assert!(s.contains("3s"));
}
#[test]
fn display_internal() {
let e = ValidatorError::Internal {
message: "null pointer".into(),
};
assert!(e.to_string().contains("null pointer"));
}
#[test]
fn category_check_failed_is_invalid_input() {
let e = ValidatorError::CheckFailed {
check_name: "x".into(),
reason: "y".into(),
};
assert_eq!(e.category(), ErrorCategory::InvalidInput);
assert!(!e.is_transient());
assert!(!e.is_retryable());
}
#[test]
fn category_policy_violation_is_invalid_input() {
let e = ValidatorError::PolicyViolation {
policy: "x".into(),
message: "y".into(),
};
assert_eq!(e.category(), ErrorCategory::InvalidInput);
assert!(!e.is_transient());
}
#[test]
fn category_missing_evidence_is_invalid_input() {
let e = ValidatorError::MissingEvidence {
expected: "x".into(),
};
assert_eq!(e.category(), ErrorCategory::InvalidInput);
}
#[test]
fn category_unavailable_is_transient_and_retryable() {
let e = ValidatorError::Unavailable {
message: "down".into(),
};
assert_eq!(e.category(), ErrorCategory::Unavailable);
assert!(e.is_transient());
assert!(e.is_retryable());
}
#[test]
fn category_timeout_is_transient_and_retryable() {
let e = ValidatorError::Timeout {
elapsed: Duration::from_secs(1),
deadline: Duration::from_secs(1),
};
assert_eq!(e.category(), ErrorCategory::Timeout);
assert!(e.is_transient());
assert!(e.is_retryable());
}
#[test]
fn category_internal_is_retryable_but_not_transient() {
let e = ValidatorError::Internal {
message: "oom".into(),
};
assert_eq!(e.category(), ErrorCategory::Internal);
assert!(!e.is_transient());
assert!(e.is_retryable());
}
#[test]
fn retry_after_always_none() {
let errors: Vec<ValidatorError> = vec![
ValidatorError::CheckFailed {
check_name: "x".into(),
reason: "y".into(),
},
ValidatorError::Unavailable {
message: "x".into(),
},
ValidatorError::Timeout {
elapsed: Duration::from_secs(1),
deadline: Duration::from_secs(1),
},
];
for e in &errors {
assert!(e.retry_after().is_none());
}
}
#[test]
fn validator_error_is_std_error() {
let e: Box<dyn std::error::Error> = Box::new(ValidatorError::Internal {
message: "test".into(),
});
assert!(e.to_string().contains("test"));
}
}