use std::collections::HashSet;
use std::fmt;
use super::FailureId;
use super::RecordedOutcome;
use super::SkipReason;
use super::SkippedValidation;
use super::ValidationLimits;
use super::ValidationOutcomeError;
use super::ValidationPath;
use super::Violation;
use super::next_report_id;
use crate::ValidationOutcome;
#[must_use]
pub struct ValidationReport {
violations: Vec<Violation>,
failure_occurrences: Vec<usize>,
skipped: Vec<SkippedValidation>,
report_id: u64,
truncated: bool,
last_recorded_occurrence: Option<usize>,
limits: ValidationLimits,
}
impl ValidationReport {
#[inline]
pub fn with_limits(limits: ValidationLimits) -> Self {
Self {
violations: Vec::new(),
failure_occurrences: Vec::new(),
skipped: Vec::new(),
report_id: next_report_id(),
truncated: false,
last_recorded_occurrence: None,
limits,
}
}
#[inline]
pub fn new() -> Self {
Self::with_limits(ValidationLimits {
max_violations: None,
max_skipped: None,
})
}
#[inline]
pub fn mark_truncated(&mut self) {
self.truncated = true;
}
pub fn record_outcome(
&mut self,
occurrence: usize,
path: ValidationPath,
outcome: ValidationOutcome,
) -> Result<RecordedOutcome, ValidationOutcomeError> {
if self.last_recorded_occurrence.is_some_and(|last| occurrence < last) {
return Err(ValidationOutcomeError::OutOfOrderOccurrence);
}
let result = match outcome {
ValidationOutcome::Valid => Ok(RecordedOutcome::new(true, Vec::new())),
ValidationOutcome::Invalid(violations) => {
if violations.is_empty() {
return Err(ValidationOutcomeError::EmptyViolations);
}
let retained = violations.len().min(self.remaining_failure_capacity());
let complete = retained == violations.len();
let mut failure_ids = Vec::with_capacity(retained);
for violation in violations.into_iter().take(retained) {
let full_path = path.concat(violation.path());
failure_ids.push(FailureId::new(self.report_id, self.violations.len()));
self.violations.push(violation.with_path(full_path));
self.failure_occurrences.push(occurrence);
}
if !complete {
self.mark_truncated();
}
Ok(RecordedOutcome::new(complete, failure_ids))
}
ValidationOutcome::Skipped {
reason: SkipReason::MissingOptional,
prerequisites,
} => {
if !prerequisites.is_empty() {
return Err(ValidationOutcomeError::UnexpectedPrerequisites);
}
if !self.has_skipped_capacity() {
self.mark_truncated();
Ok(RecordedOutcome::new(false, Vec::new()))
} else {
self.skipped.push(SkippedValidation::missing_optional(occurrence, path));
Ok(RecordedOutcome::new(true, Vec::new()))
}
}
ValidationOutcome::Skipped {
reason: SkipReason::FailedPrerequisite,
prerequisites,
} => {
if prerequisites.is_empty() {
return Err(ValidationOutcomeError::EmptyPrerequisites);
}
let mut unique = HashSet::with_capacity(prerequisites.len());
for prerequisite in &prerequisites {
if !unique.insert(*prerequisite) {
return Err(ValidationOutcomeError::DuplicatePrerequisiteFailure);
}
if prerequisite.report != self.report_id || self.violations.get(prerequisite.index).is_none() {
return Err(ValidationOutcomeError::UnknownPrerequisiteFailure);
}
}
if !self.has_skipped_capacity() {
self.mark_truncated();
Ok(RecordedOutcome::new(false, Vec::new()))
} else {
let skipped = SkippedValidation::failed_prerequisite(occurrence, path, prerequisites)?;
self.skipped.push(skipped);
Ok(RecordedOutcome::new(true, Vec::new()))
}
}
};
if result.is_ok() {
self.last_recorded_occurrence = Some(occurrence);
}
result
}
#[must_use]
pub fn is_valid(&self) -> bool {
self.violations.is_empty()
&& !self.truncated
&& self
.skipped
.iter()
.all(|item| item.reason() != SkipReason::FailedPrerequisite)
}
#[must_use = "the collected violations should be inspected"]
#[inline]
pub fn violations(&self) -> &[Violation] {
&self.violations
}
#[must_use = "inspect all retained failure evidence"]
#[inline]
pub fn failures(&self) -> impl Iterator<Item = &Violation> + '_ {
self.violations.iter()
}
#[must_use]
#[inline]
pub const fn failure_count(&self) -> usize {
self.violations.len()
}
#[must_use]
#[inline]
pub fn failure(&self, id: FailureId) -> Option<&Violation> {
if id.report == self.report_id {
self.violations.get(id.index)
} else {
None
}
}
#[must_use]
#[inline]
pub fn failure_occurrence(&self, id: FailureId) -> Option<usize> {
self.failure(id)?;
self.failure_occurrences.get(id.index).copied()
}
#[must_use = "inspect all retained failures and their occurrences"]
#[inline]
pub fn failures_with_occurrences(&self) -> impl Iterator<Item = (usize, &Violation)> + '_ {
self.failure_occurrences.iter().copied().zip(self.violations.iter())
}
#[must_use = "the skipped occurrences should be inspected"]
#[inline]
pub fn skipped(&self) -> &[SkippedValidation] {
&self.skipped
}
#[must_use]
#[inline]
pub const fn is_truncated(&self) -> bool {
self.truncated
}
#[must_use]
#[inline]
pub const fn limits(&self) -> ValidationLimits {
self.limits
}
fn remaining_failure_capacity(&self) -> usize {
self.limits
.max_violations
.map_or(usize::MAX, |limit| limit.saturating_sub(self.violations.len()))
}
fn has_skipped_capacity(&self) -> bool {
self.limits.max_skipped.is_none_or(|limit| self.skipped.len() < limit)
}
}
impl Default for ValidationReport {
#[inline]
fn default() -> Self {
Self::new()
}
}
impl fmt::Debug for ValidationReport {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ValidationReport")
.field("violation_count", &self.failure_count())
.field("skipped_count", &self.skipped.len())
.field("truncated", &self.truncated)
.finish()
}
}
impl fmt::Display for ValidationReport {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"validation report: {} violation(s), {} skipped, truncated={}",
self.failure_count(),
self.skipped.len(),
self.truncated,
)
}
}