qubit-metadata 0.7.2

Type-safe metadata model with schemas and composable filters
Documentation
// =============================================================================
//    Copyright (c) 2025 - 2026 Haixing Hu.
//
//    SPDX-License-Identifier: Apache-2.0
//
//    Licensed under the Apache License, Version 2.0.
// =============================================================================
//! Errors from bounded JSON metadata wire decoding.

use std::error::Error;
use std::fmt;
use std::io;
use std::sync::Arc;

use qubit_budget::BudgetError;
use qubit_budget::MeasuredBudgetError;
use qubit_budget::QuantityConversionError;
use qubit_budget::json::JsonResource;
use qubit_json::decode::JsonDecodeError;
use qubit_json::decode::JsonDecodeErrorSource;
use qubit_json::decode::JsonSyntaxError;

use crate::MetadataError;

/// Failure returned by a bounded metadata JSON decoding API.
///
/// # Examples
///
/// ```
/// use qubit_metadata::Metadata;
///
/// let error = Metadata::decode_json_slice(b"not-json").unwrap_err();
/// assert!(!error.to_string().is_empty());
/// ```
#[derive(Debug)]
#[non_exhaustive]
#[must_use]
pub enum MetadataWireDecodeError {
    /// Reading the JSON wire input failed.
    Read {
        /// Stable standard I/O failure category.
        kind: io::ErrorKind,
        /// Original error retained for detailed diagnostics.
        source: Option<Arc<io::Error>>,
    },
    /// A metadata map or schema exceeded a domain limit without exposing
    /// values.
    Domain(
        /// Structured metadata-domain failure.
        MetadataError,
    ),
    /// The envelope requests a wire version this decoder does not support.
    UnsupportedVersion {
        /// Version supported by this decoder.
        expected: u8,
        /// Version present in the envelope.
        actual: u8,
    },
    /// The JSON document exceeded one shared budget limit.
    Budget(
        /// Shared JSON budget that was exceeded.
        BudgetError<JsonResource, usize>,
    ),
    /// A native JSON measurement could not be represented by the budget
    /// quantity.
    Quantity {
        /// Resource whose measurement failed to convert.
        resource: JsonResource,
        /// Conversion failure retaining the original measurement and target
        /// type.
        source: QuantityConversionError,
    },
    /// The JSON lexical preflight rejected the document with source details.
    Syntax(
        /// Lexical JSON syntax failure.
        JsonSyntaxError,
    ),
    /// The caller supplied metadata limits outside the protocol hard caps.
    InvalidLimits(
        /// Validation error for the supplied metadata limits.
        serde_json::Error,
    ),
    /// A decoded metadata-filter envelope violated its structured contract.
    #[cfg(feature = "filter")]
    Filter(
        /// Structured filter-contract failure.
        MetadataError,
    ),
    /// A materialized metadata wire value could not be converted to a scalar
    /// metadata value.
    InvalidJson(
        /// The original serde_json error.
        serde_json::Error,
    ),
    /// A shared decoding failure retaining its structured facts and
    /// diagnostic policy.
    Decode(
        /// Shared JSON decoding failure.
        JsonDecodeError<JsonResource>,
    ),
}

impl fmt::Display for MetadataWireDecodeError {
    /// Formats a bounded JSON decoding failure.
    ///
    /// # Parameters
    ///
    /// * `formatter` - Formatter receiving the error message.
    ///
    /// # Returns
    ///
    /// A formatting result indicating whether the message was written.
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Read { kind, .. } => {
                write!(formatter, "Failed to read metadata JSON input: {kind:?}")
            }
            Self::Domain(error) => fmt::Display::fmt(error, formatter),
            Self::UnsupportedVersion { expected, actual } => write!(
                formatter,
                "Unsupported metadata wire version {actual}; expected {expected}"
            ),
            Self::Budget(error) => fmt::Display::fmt(error, formatter),
            Self::Quantity { source, .. } => fmt::Display::fmt(source, formatter),
            Self::Syntax(error) => fmt::Display::fmt(error, formatter),
            Self::InvalidLimits(error) => fmt::Display::fmt(error, formatter),
            #[cfg(feature = "filter")]
            Self::Filter(error) => fmt::Display::fmt(error, formatter),
            Self::InvalidJson(error) => fmt::Display::fmt(error, formatter),
            Self::Decode(error) => fmt::Display::fmt(error, formatter),
        }
    }
}

impl Error for MetadataWireDecodeError {
    /// Returns the retained source error associated with this failure.
    ///
    /// # Returns
    ///
    /// Returns `Some` for failures that retain an underlying source, including
    /// detailed read failures, domain, budget, quantity, syntax, Serde, and
    /// optional filter failures. Returns `None` for unsupported wire versions
    /// and read failures whose diagnostic policy did not retain the original
    /// I/O error.
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            Self::Read { source, .. } => source.as_deref().map(|error| error as &(dyn Error + 'static)),
            Self::Domain(error) => Some(error),
            Self::UnsupportedVersion { .. } => None,
            Self::Budget(error) => Some(error),
            Self::Quantity { source, .. } => Some(source),
            Self::Syntax(error) => Some(error),
            Self::InvalidLimits(error) => Some(error),
            #[cfg(feature = "filter")]
            Self::Filter(error) => Some(error),
            Self::InvalidJson(error) => Some(error),
            Self::Decode(error) => Some(error),
        }
    }
}

impl From<JsonDecodeError<JsonResource>> for MetadataWireDecodeError {
    /// Converts a shared JSON decoding error into a metadata wire error.
    ///
    /// # Parameters
    ///
    /// * `error` - Shared JSON decoding failure to convert.
    ///
    /// # Returns
    ///
    /// The corresponding metadata wire decoding error.
    fn from(error: JsonDecodeError<JsonResource>) -> Self {
        match error.clone().into_source() {
            JsonDecodeErrorSource::Read { kind, source, .. } => Self::Read { kind, source },
            JsonDecodeErrorSource::Budget { source, .. } => match source {
                MeasuredBudgetError::Budget(error) => Self::Budget(error),
                MeasuredBudgetError::Quantity { resource, source } => Self::Quantity { resource, source },
            },
            JsonDecodeErrorSource::InvalidJson { syntax, .. } => Self::Syntax(syntax),
            _ => Self::Decode(error),
        }
    }
}