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 crate::metadata_error::MetadataError;
13
14/// Aggregate error returned by schema-level validation APIs.
15///
16/// Unlike single-entry metadata accessors, schema validation can discover
17/// multiple independent issues in one pass. This type preserves all collected
18/// [`MetadataError`] values so callers can report or fix them together.
19///
20/// # Examples
21///
22/// ```
23/// use qubit_metadata::{MetadataError, MetadataValidationError};
24///
25/// let error = MetadataValidationError::from_issue(MetadataError::MissingKey("tenant".into()));
26/// assert_eq!(error.len(), 1);
27/// ```
28#[derive(Debug, Clone, PartialEq, Eq)]
29#[must_use]
30pub struct MetadataValidationError {
31 /// Collected validation issues.
32 issues: Vec<MetadataError>,
33}
34
35impl MetadataValidationError {
36 /// Creates an aggregate validation error from one issue.
37 ///
38 /// # Parameters
39 ///
40 /// * `issue` - Validation issue to store.
41 ///
42 /// # Returns
43 ///
44 /// An aggregate error containing exactly one issue.
45 #[inline]
46 #[must_use = "the validation error should be inspected"]
47 pub fn from_issue(issue: MetadataError) -> Self {
48 Self { issues: vec![issue] }
49 }
50
51 /// Creates an aggregate validation error from a non-empty issue list.
52 ///
53 /// Returns `None` when `issues` is empty because an empty validation error
54 /// has no actionable meaning.
55 ///
56 /// # Parameters
57 ///
58 /// * `issues` - Validation issues to aggregate.
59 ///
60 /// # Returns
61 ///
62 /// `Some` aggregate error for a non-empty list; otherwise, `None`.
63 #[inline]
64 pub fn from_issues(issues: Vec<MetadataError>) -> Option<Self> {
65 (!issues.is_empty()).then_some(Self { issues })
66 }
67
68 /// Returns the collected validation issues.
69 ///
70 /// # Returns
71 ///
72 /// The issues in discovery order.
73 #[inline]
74 #[must_use = "the validation issues should be inspected"]
75 pub fn issues(&self) -> &[MetadataError] {
76 &self.issues
77 }
78
79 /// Returns the number of collected validation issues.
80 ///
81 /// # Returns
82 ///
83 /// The issue count.
84 #[inline]
85 #[must_use]
86 #[allow(clippy::len_without_is_empty)]
87 pub fn len(&self) -> usize {
88 self.issues.len()
89 }
90
91 /// Converts this aggregate error into its collected issues.
92 ///
93 /// # Returns
94 ///
95 /// The owned issues in discovery order.
96 #[inline]
97 #[must_use]
98 pub fn into_issues(self) -> Vec<MetadataError> {
99 self.issues
100 }
101}
102
103impl fmt::Display for MetadataValidationError {
104 /// Formats the validation summary and every collected issue.
105 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
106 write!(formatter, "{} metadata validation issue(s)", self.issues.len())?;
107 for (index, issue) in self.issues.iter().enumerate() {
108 write!(formatter, "; {}: {issue}", index + 1)?;
109 }
110 Ok(())
111 }
112}
113
114impl std::error::Error for MetadataValidationError {}