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}