qubit-metadata 0.7.2

Type-safe metadata model with schemas and composable filters
Documentation
// =============================================================================
//    Copyright (c) 2025 - 2026 Haixing Hu.
//
//    SPDX-License-Identifier: Apache-2.0
//
//    Licensed under the Apache License, Version 2.0.
// =============================================================================
//! [`MetadataValidationError`] — aggregate schema validation failures.

use std::fmt;

use qubit_redact::Redact;
use qubit_redact::RedactionWriter;
use qubit_redact::Redactor;

use crate::metadata_error::MetadataError;

/// Aggregate error returned by schema-level validation APIs.
///
/// Unlike single-entry metadata accessors, schema validation can discover
/// multiple independent issues in one pass. This type preserves all collected
/// [`MetadataError`] values so callers can report or fix them together.
/// `Debug` and `Display` share a bounded diagnostic traversal; use
/// [`Self::issues`] to inspect the complete collection independently of
/// formatting limits.
///
/// # Examples
///
/// ```
/// use qubit_metadata::{MetadataError, MetadataValidationError};
///
/// let error = MetadataValidationError::from_issue(MetadataError::MissingKey("tenant".into()));
/// assert_eq!(error.len(), 1);
/// ```
#[derive(Clone, PartialEq, Eq)]
#[must_use]
pub struct MetadataValidationError {
    /// Collected validation issues.
    issues: Vec<MetadataError>,
}

impl MetadataValidationError {
    /// Creates an aggregate validation error from one issue.
    ///
    /// # Parameters
    ///
    /// * `issue` - Validation issue to store.
    ///
    /// # Returns
    ///
    /// An aggregate error containing exactly one issue.
    #[inline]
    pub fn from_issue(issue: MetadataError) -> Self {
        Self { issues: vec![issue] }
    }

    /// Creates an aggregate validation error from a non-empty issue list.
    ///
    /// Returns `None` when `issues` is empty because an empty validation error
    /// has no actionable meaning.
    ///
    /// # Parameters
    ///
    /// * `issues` - Validation issues to aggregate.
    ///
    /// # Returns
    ///
    /// `Some` aggregate error for a non-empty list; otherwise, `None`.
    #[must_use]
    #[inline]
    pub fn from_issues(issues: Vec<MetadataError>) -> Option<Self> {
        (!issues.is_empty()).then_some(Self { issues })
    }

    /// Returns the collected validation issues.
    ///
    /// # Returns
    ///
    /// The issues in discovery order.
    #[inline]
    #[must_use = "the validation issues should be inspected"]
    pub fn issues(&self) -> &[MetadataError] {
        &self.issues
    }

    /// Returns the number of collected validation issues.
    ///
    /// # Returns
    ///
    /// The issue count.
    #[inline]
    #[must_use]
    #[allow(clippy::len_without_is_empty)]
    pub fn len(&self) -> usize {
        self.issues.len()
    }

    /// Converts this aggregate error into its collected issues.
    ///
    /// # Returns
    ///
    /// The owned issues in discovery order.
    #[inline]
    #[must_use]
    pub fn into_issues(self) -> Vec<MetadataError> {
        self.issues
    }
}

impl Redact for MetadataValidationError {
    /// Writes admitted issues under one cumulative diagnostic budget.
    ///
    /// Issue text is diagnostic data, not a confidentiality boundary for
    /// arbitrary keys or application-supplied error messages.
    ///
    /// # Parameters
    ///
    /// * `writer` - Destination sharing output, node, and collection budgets.
    fn write_redacted(&self, writer: &mut RedactionWriter<'_>) {
        writer.sequence(|items| {
            items.for_each(&self.issues, |items, issue| {
                items.unredacted_item(|| issue);
            });
        });
    }
}

impl fmt::Debug for MetadataValidationError {
    /// Formats a bounded diagnostic summary using the strict policy.
    ///
    /// # Parameters
    ///
    /// * `formatter` - Destination receiving the diagnostic summary.
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt::Display::fmt(self, formatter)
    }
}

impl fmt::Display for MetadataValidationError {
    /// Formats the issue count and a bounded diagnostic view of the issues.
    ///
    /// The complete issue collection remains available through `issues()`.
    /// Exhausted diagnostic budgets produce an explicit incomplete marker.
    ///
    /// # Parameters
    ///
    /// * `formatter` - Formatter receiving the validation summary.
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        let output = Redactor::strict().redact_text(self);
        let text = output.text_or_marker("<redaction incomplete>");
        write!(
            formatter,
            "{} metadata validation issue(s): {}",
            self.issues.len(),
            text
        )
    }
}

impl std::error::Error for MetadataValidationError {}