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;
12
13use qubit_budget::BudgetError;
14use qubit_budget::MeasuredBudgetError;
15use qubit_budget::QuantityConversionError;
16use qubit_budget::json::JsonResource;
17use qubit_json::decode::JsonDecodeError;
18use qubit_json::decode::JsonDecodeErrorSource;
19use qubit_json::decode::JsonSyntaxError;
20use serde::de::Error as DeError;
21
22use crate::MetadataError;
23
24/// Failure returned by a bounded metadata JSON decoding API.
25///
26/// # Examples
27///
28/// ```
29/// use qubit_metadata::Metadata;
30///
31/// let error = Metadata::decode_json_slice(b"not-json").unwrap_err();
32/// assert!(!error.to_string().is_empty());
33/// ```
34#[derive(Debug)]
35#[non_exhaustive]
36#[must_use]
37pub enum MetadataWireDecodeError {
38    /// A metadata map or schema exceeded a domain limit without exposing
39    /// values.
40    Domain(MetadataError),
41    /// The envelope requests a wire version this decoder does not support.
42    UnsupportedVersion {
43        /// Version supported by this decoder.
44        expected: u8,
45        /// Version present in the envelope.
46        actual: u8,
47    },
48    /// The JSON document exceeded one shared budget limit.
49    Budget(BudgetError<JsonResource, usize>),
50    /// A native JSON measurement could not be represented by the budget
51    /// quantity.
52    Quantity {
53        /// Resource whose measurement failed to convert.
54        resource: JsonResource,
55        /// Conversion failure retaining the original measurement and target
56        /// type.
57        source: QuantityConversionError,
58    },
59    /// The JSON lexical preflight rejected the document with source details.
60    Syntax(JsonSyntaxError),
61    /// The caller supplied metadata limits outside the protocol hard caps.
62    InvalidLimits(serde_json::Error),
63    /// A decoded metadata-filter envelope violated its structured contract.
64    #[cfg(feature = "filter")]
65    Filter(MetadataError),
66    /// JSON syntax or strict wire-envelope decoding failed after the byte limit
67    /// check.
68    InvalidJson(
69        /// The original serde_json error.
70        serde_json::Error,
71    ),
72}
73
74impl fmt::Display for MetadataWireDecodeError {
75    /// Formats a bounded JSON decoding failure.
76    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
77        match self {
78            Self::Domain(error) => fmt::Display::fmt(error, formatter),
79            Self::UnsupportedVersion { expected, actual } => write!(
80                formatter,
81                "Unsupported metadata wire version {actual}; expected {expected}"
82            ),
83            Self::Budget(error) => fmt::Display::fmt(error, formatter),
84            Self::Quantity { source, .. } => fmt::Display::fmt(source, formatter),
85            Self::Syntax(error) => fmt::Display::fmt(error, formatter),
86            Self::InvalidLimits(error) => fmt::Display::fmt(error, formatter),
87            #[cfg(feature = "filter")]
88            Self::Filter(error) => fmt::Display::fmt(error, formatter),
89            Self::InvalidJson(error) => fmt::Display::fmt(error, formatter),
90        }
91    }
92}
93
94impl Error for MetadataWireDecodeError {
95    /// Returns the budget, filter, or Serde source associated with the error.
96    fn source(&self) -> Option<&(dyn Error + 'static)> {
97        match self {
98            Self::Domain(error) => Some(error),
99            Self::UnsupportedVersion { .. } => None,
100            Self::Budget(error) => Some(error),
101            Self::Quantity { source, .. } => Some(source),
102            Self::Syntax(error) => Some(error),
103            Self::InvalidLimits(error) => Some(error),
104            #[cfg(feature = "filter")]
105            Self::Filter(error) => Some(error),
106            Self::InvalidJson(error) => Some(error),
107        }
108    }
109}
110
111impl From<JsonDecodeError<JsonResource>> for MetadataWireDecodeError {
112    /// Converts a shared JSON decoding error into a metadata wire error.
113    fn from(error: JsonDecodeError<JsonResource>) -> Self {
114        let kind = error.kind();
115        let line = error.line().unwrap_or(0);
116        let column = error.column().unwrap_or(0);
117        match error.into_source() {
118            JsonDecodeErrorSource::Budget { source, .. } => match source {
119                MeasuredBudgetError::Budget(error) => Self::Budget(error),
120                MeasuredBudgetError::Quantity { resource, source } => Self::Quantity { resource, source },
121            },
122            JsonDecodeErrorSource::InvalidJson { syntax, .. } => Self::Syntax(syntax),
123            JsonDecodeErrorSource::EmptyInput { .. }
124            | JsonDecodeErrorSource::InvalidUtf8 { .. }
125            | JsonDecodeErrorSource::UnexpectedTopLevel { .. }
126            | JsonDecodeErrorSource::Deserialize { .. } => Self::InvalidJson(<serde_json::Error as DeError>::custom(
127                format_args!("JSON decoding failed ({kind:?}) at line {line}, column {column}",),
128            )),
129        }
130    }
131}