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 {}