Skip to main content

qubit_metadata/
metadata_validation_error.rs

1// =============================================================================
2//    Copyright (c) 2025 - 2026 Haixing Hu.
3//
4//    SPDX-License-Identifier: Apache-2.0
5//
6//    Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! [`MetadataValidationError`] — aggregate schema validation failures.
9
10use std::fmt;
11
12use qubit_redact::Redact;
13use qubit_redact::RedactionWriter;
14use qubit_redact::Redactor;
15
16use crate::metadata_error::MetadataError;
17
18/// Aggregate error returned by schema-level validation APIs.
19///
20/// Unlike single-entry metadata accessors, schema validation can discover
21/// multiple independent issues in one pass. This type preserves all collected
22/// [`MetadataError`] values so callers can report or fix them together.
23/// `Debug` and `Display` share a bounded diagnostic traversal; use
24/// [`Self::issues`] to inspect the complete collection independently of
25/// formatting limits.
26///
27/// # Examples
28///
29/// ```
30/// use qubit_metadata::{MetadataError, MetadataValidationError};
31///
32/// let error = MetadataValidationError::from_issue(MetadataError::MissingKey("tenant".into()));
33/// assert_eq!(error.len(), 1);
34/// ```
35#[derive(Clone, PartialEq, Eq)]
36#[must_use]
37pub struct MetadataValidationError {
38    /// Collected validation issues.
39    issues: Vec<MetadataError>,
40}
41
42impl MetadataValidationError {
43    /// Creates an aggregate validation error from one issue.
44    ///
45    /// # Parameters
46    ///
47    /// * `issue` - Validation issue to store.
48    ///
49    /// # Returns
50    ///
51    /// An aggregate error containing exactly one issue.
52    #[inline]
53    pub fn from_issue(issue: MetadataError) -> Self {
54        Self { issues: vec![issue] }
55    }
56
57    /// Creates an aggregate validation error from a non-empty issue list.
58    ///
59    /// Returns `None` when `issues` is empty because an empty validation error
60    /// has no actionable meaning.
61    ///
62    /// # Parameters
63    ///
64    /// * `issues` - Validation issues to aggregate.
65    ///
66    /// # Returns
67    ///
68    /// `Some` aggregate error for a non-empty list; otherwise, `None`.
69    #[must_use]
70    #[inline]
71    pub fn from_issues(issues: Vec<MetadataError>) -> Option<Self> {
72        (!issues.is_empty()).then_some(Self { issues })
73    }
74
75    /// Returns the collected validation issues.
76    ///
77    /// # Returns
78    ///
79    /// The issues in discovery order.
80    #[inline]
81    #[must_use = "the validation issues should be inspected"]
82    pub fn issues(&self) -> &[MetadataError] {
83        &self.issues
84    }
85
86    /// Returns the number of collected validation issues.
87    ///
88    /// # Returns
89    ///
90    /// The issue count.
91    #[inline]
92    #[must_use]
93    #[allow(clippy::len_without_is_empty)]
94    pub fn len(&self) -> usize {
95        self.issues.len()
96    }
97
98    /// Converts this aggregate error into its collected issues.
99    ///
100    /// # Returns
101    ///
102    /// The owned issues in discovery order.
103    #[inline]
104    #[must_use]
105    pub fn into_issues(self) -> Vec<MetadataError> {
106        self.issues
107    }
108}
109
110impl Redact for MetadataValidationError {
111    /// Writes admitted issues under one cumulative diagnostic budget.
112    ///
113    /// Issue text is diagnostic data, not a confidentiality boundary for
114    /// arbitrary keys or application-supplied error messages.
115    ///
116    /// # Parameters
117    ///
118    /// * `writer` - Destination sharing output, node, and collection budgets.
119    fn write_redacted(&self, writer: &mut RedactionWriter<'_>) {
120        writer.sequence(|items| {
121            items.for_each(&self.issues, |items, issue| {
122                items.unredacted_item(|| issue);
123            });
124        });
125    }
126}
127
128impl fmt::Debug for MetadataValidationError {
129    /// Formats a bounded diagnostic summary using the strict policy.
130    ///
131    /// # Parameters
132    ///
133    /// * `formatter` - Destination receiving the diagnostic summary.
134    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
135        fmt::Display::fmt(self, formatter)
136    }
137}
138
139impl fmt::Display for MetadataValidationError {
140    /// Formats the issue count and a bounded diagnostic view of the issues.
141    ///
142    /// The complete issue collection remains available through `issues()`.
143    /// Exhausted diagnostic budgets produce an explicit incomplete marker.
144    ///
145    /// # Parameters
146    ///
147    /// * `formatter` - Formatter receiving the validation summary.
148    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
149        let output = Redactor::strict().redact_text(self);
150        let text = output.text_or_marker("<redaction incomplete>");
151        write!(
152            formatter,
153            "{} metadata validation issue(s): {}",
154            self.issues.len(),
155            text
156        )
157    }
158}
159
160impl std::error::Error for MetadataValidationError {}