qubit-metadata 0.6.1

Type-safe metadata model with schemas and composable filters
// =============================================================================
//    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 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 serde::de::Error as DeError;

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 {
    /// A metadata map or schema exceeded a domain limit without exposing
    /// values.
    Domain(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(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(JsonSyntaxError),
    /// The caller supplied metadata limits outside the protocol hard caps.
    InvalidLimits(serde_json::Error),
    /// A decoded metadata-filter envelope violated its structured contract.
    #[cfg(feature = "filter")]
    Filter(MetadataError),
    /// JSON syntax or strict wire-envelope decoding failed after the byte limit
    /// check.
    InvalidJson(
        /// The original serde_json error.
        serde_json::Error,
    ),
}

impl fmt::Display for MetadataWireDecodeError {
    /// Formats a bounded JSON decoding failure.
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            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),
        }
    }
}

impl Error for MetadataWireDecodeError {
    /// Returns the budget, filter, or Serde source associated with the error.
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            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),
        }
    }
}

impl From<JsonDecodeError<JsonResource>> for MetadataWireDecodeError {
    /// Converts a shared JSON decoding error into a metadata wire error.
    fn from(error: JsonDecodeError<JsonResource>) -> Self {
        let kind = error.kind();
        let line = error.line().unwrap_or(0);
        let column = error.column().unwrap_or(0);
        match error.into_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),
            JsonDecodeErrorSource::EmptyInput { .. }
            | JsonDecodeErrorSource::InvalidUtf8 { .. }
            | JsonDecodeErrorSource::UnexpectedTopLevel { .. }
            | JsonDecodeErrorSource::Deserialize { .. } => Self::InvalidJson(<serde_json::Error as DeError>::custom(
                format_args!("JSON decoding failed ({kind:?}) at line {line}, column {column}",),
            )),
        }
    }
}