Skip to main content

qubit_metadata/
metadata_wire_decode_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//! Errors from bounded JSON metadata wire decoding.
9
10use std::error::Error;
11use std::fmt;
12use std::io;
13use std::sync::Arc;
14
15use qubit_budget::BudgetError;
16use qubit_budget::MeasuredBudgetError;
17use qubit_budget::QuantityConversionError;
18use qubit_budget::json::JsonResource;
19use qubit_json::decode::JsonDecodeError;
20use qubit_json::decode::JsonDecodeErrorSource;
21use qubit_json::decode::JsonSyntaxError;
22
23use crate::MetadataError;
24
25/// Failure returned by a bounded metadata JSON decoding API.
26///
27/// # Examples
28///
29/// ```
30/// use qubit_metadata::Metadata;
31///
32/// let error = Metadata::decode_json_slice(b"not-json").unwrap_err();
33/// assert!(!error.to_string().is_empty());
34/// ```
35#[derive(Debug)]
36#[non_exhaustive]
37#[must_use]
38pub enum MetadataWireDecodeError {
39    /// Reading the JSON wire input failed.
40    Read {
41        /// Stable standard I/O failure category.
42        kind: io::ErrorKind,
43        /// Original error retained for detailed diagnostics.
44        source: Option<Arc<io::Error>>,
45    },
46    /// A metadata map or schema exceeded a domain limit without exposing
47    /// values.
48    Domain(
49        /// Structured metadata-domain failure.
50        MetadataError,
51    ),
52    /// The envelope requests a wire version this decoder does not support.
53    UnsupportedVersion {
54        /// Version supported by this decoder.
55        expected: u8,
56        /// Version present in the envelope.
57        actual: u8,
58    },
59    /// The JSON document exceeded one shared budget limit.
60    Budget(
61        /// Shared JSON budget that was exceeded.
62        BudgetError<JsonResource, usize>,
63    ),
64    /// A native JSON measurement could not be represented by the budget
65    /// quantity.
66    Quantity {
67        /// Resource whose measurement failed to convert.
68        resource: JsonResource,
69        /// Conversion failure retaining the original measurement and target
70        /// type.
71        source: QuantityConversionError,
72    },
73    /// The JSON lexical preflight rejected the document with source details.
74    Syntax(
75        /// Lexical JSON syntax failure.
76        JsonSyntaxError,
77    ),
78    /// The caller supplied metadata limits outside the protocol hard caps.
79    InvalidLimits(
80        /// Validation error for the supplied metadata limits.
81        serde_json::Error,
82    ),
83    /// A decoded metadata-filter envelope violated its structured contract.
84    #[cfg(feature = "filter")]
85    Filter(
86        /// Structured filter-contract failure.
87        MetadataError,
88    ),
89    /// A materialized metadata wire value could not be converted to a scalar
90    /// metadata value.
91    InvalidJson(
92        /// The original serde_json error.
93        serde_json::Error,
94    ),
95    /// A shared decoding failure retaining its structured facts and
96    /// diagnostic policy.
97    Decode(
98        /// Shared JSON decoding failure.
99        JsonDecodeError<JsonResource>,
100    ),
101}
102
103impl fmt::Display for MetadataWireDecodeError {
104    /// Formats a bounded JSON decoding failure.
105    ///
106    /// # Parameters
107    ///
108    /// * `formatter` - Formatter receiving the error message.
109    ///
110    /// # Returns
111    ///
112    /// A formatting result indicating whether the message was written.
113    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
114        match self {
115            Self::Read { kind, .. } => {
116                write!(formatter, "Failed to read metadata JSON input: {kind:?}")
117            }
118            Self::Domain(error) => fmt::Display::fmt(error, formatter),
119            Self::UnsupportedVersion { expected, actual } => write!(
120                formatter,
121                "Unsupported metadata wire version {actual}; expected {expected}"
122            ),
123            Self::Budget(error) => fmt::Display::fmt(error, formatter),
124            Self::Quantity { source, .. } => fmt::Display::fmt(source, formatter),
125            Self::Syntax(error) => fmt::Display::fmt(error, formatter),
126            Self::InvalidLimits(error) => fmt::Display::fmt(error, formatter),
127            #[cfg(feature = "filter")]
128            Self::Filter(error) => fmt::Display::fmt(error, formatter),
129            Self::InvalidJson(error) => fmt::Display::fmt(error, formatter),
130            Self::Decode(error) => fmt::Display::fmt(error, formatter),
131        }
132    }
133}
134
135impl Error for MetadataWireDecodeError {
136    /// Returns the retained source error associated with this failure.
137    ///
138    /// # Returns
139    ///
140    /// Returns `Some` for failures that retain an underlying source, including
141    /// detailed read failures, domain, budget, quantity, syntax, Serde, and
142    /// optional filter failures. Returns `None` for unsupported wire versions
143    /// and read failures whose diagnostic policy did not retain the original
144    /// I/O error.
145    fn source(&self) -> Option<&(dyn Error + 'static)> {
146        match self {
147            Self::Read { source, .. } => source.as_deref().map(|error| error as &(dyn Error + 'static)),
148            Self::Domain(error) => Some(error),
149            Self::UnsupportedVersion { .. } => None,
150            Self::Budget(error) => Some(error),
151            Self::Quantity { source, .. } => Some(source),
152            Self::Syntax(error) => Some(error),
153            Self::InvalidLimits(error) => Some(error),
154            #[cfg(feature = "filter")]
155            Self::Filter(error) => Some(error),
156            Self::InvalidJson(error) => Some(error),
157            Self::Decode(error) => Some(error),
158        }
159    }
160}
161
162impl From<JsonDecodeError<JsonResource>> for MetadataWireDecodeError {
163    /// Converts a shared JSON decoding error into a metadata wire error.
164    ///
165    /// # Parameters
166    ///
167    /// * `error` - Shared JSON decoding failure to convert.
168    ///
169    /// # Returns
170    ///
171    /// The corresponding metadata wire decoding error.
172    fn from(error: JsonDecodeError<JsonResource>) -> Self {
173        match error.clone().into_source() {
174            JsonDecodeErrorSource::Read { kind, source, .. } => Self::Read { kind, source },
175            JsonDecodeErrorSource::Budget { source, .. } => match source {
176                MeasuredBudgetError::Budget(error) => Self::Budget(error),
177                MeasuredBudgetError::Quantity { resource, source } => Self::Quantity { resource, source },
178            },
179            JsonDecodeErrorSource::InvalidJson { syntax, .. } => Self::Syntax(syntax),
180            _ => Self::Decode(error),
181        }
182    }
183}