qubit-value 0.11.0

Type-safe containers for single, multi-valued, and named runtime values
Documentation
// =============================================================================
//    Copyright (c) 2025 - 2026 Haixing Hu.
//
//    SPDX-License-Identifier: Apache-2.0
//
//    Licensed under the Apache License, Version 2.0.
// =============================================================================

//! Public DTO for the stable version-one JSON wire contract.

#[cfg(feature = "json")]
use std::io::Write;

#[cfg(feature = "json")]
use qubit_budget::json::JsonDecodeLimits;
#[cfg(feature = "json")]
use qubit_budget::json::JsonDecodeSession;
#[cfg(feature = "json")]
use qubit_budget::json::JsonEncodeLimits;
#[cfg(feature = "json")]
use qubit_budget::json::JsonEncodeSession;
#[cfg(feature = "json")]
use qubit_json::encode::JsonEncoder;
use serde::Serialize;
use serde::Serializer;

use super::VALUE_WIRE_V1_VERSION;
#[cfg(feature = "json")]
use super::ValueWireDecodeError;
use super::ValueWireEncodeError;
use super::ValueWirePayloadV1;
use super::serialize_wire;
use crate::MultiValues;
use crate::Value;
use crate::ValueContainer;

/// Stable version-one wire DTO for a scalar or homogeneous collection.
///
/// For a given serializer and value, output is byte-stable with canonical field
/// and object-key order, including recursively nested JSON objects. V1 is
/// closed: existing tags, shapes, and payload representations cannot change,
/// and future runtime data types require a new wire version instead of
/// extending V1.
///
/// Deserialization is intentionally available through
/// [`crate::ValueWireV1Seed`], which lets a bounded decoder control the
/// complete input and structure.
///
/// # Examples
///
/// ```
/// use std::convert::TryFrom;
/// use qubit_value::{Value, ValueWireV1};
///
/// let _wire = ValueWireV1::try_from(Value::from(42_i32)).unwrap();
/// ```
#[must_use]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ValueWireV1 {
    /// Explicit runtime shape and typed payload represented by this DTO.
    value: ValueWirePayloadV1,
}

impl ValueWireV1 {
    /// Numeric version emitted and accepted by this DTO.
    pub const VERSION: u8 = VALUE_WIRE_V1_VERSION;

    /// Creates a V1 DTO from an explicit scalar-or-collection container.
    ///
    /// # Parameters
    ///
    /// * `value` - Runtime container whose exact type and shape are preserved.
    ///
    /// # Returns
    ///
    /// A V1 DTO containing `value`.
    #[inline(always)]
    pub const fn new(value: ValueWirePayloadV1) -> Self {
        Self { value }
    }

    /// Returns the default JSON resource profile for complete V1 documents.
    ///
    /// # Returns
    ///
    /// Decode limits suitable for one standalone V1 envelope.
    #[cfg(feature = "json")]
    #[must_use = "the V1 JSON profile should be applied to a budget"]
    #[inline(always)]
    pub fn default_json_decode_limits() -> JsonDecodeLimits {
        super::default_json_decode_limits()
    }

    /// Returns the default JSON resource profile for encoding V1 documents.
    ///
    /// # Returns
    ///
    /// Encode limits suitable for one standalone V1 envelope.
    #[cfg(feature = "json")]
    #[must_use = "the V1 JSON profile should be applied to an encode session"]
    #[inline(always)]
    pub fn default_json_encode_limits() -> JsonEncodeLimits {
        super::default_json_encode_limits()
    }

    /// Decodes a V1 JSON wire value using the default structural limits.
    ///
    /// The complete input length and decoded structure are checked before the
    /// value is returned. Embedded protocols should share one
    /// [`qubit_budget::json::JsonDecodeSession`] across their complete
    /// document.
    ///
    /// # Parameters
    ///
    /// * `input` - Complete UTF-8 JSON document to decode.
    ///
    /// # Returns
    ///
    /// The decoded V1 wire DTO.
    ///
    /// # Errors
    ///
    /// Returns a limit error when the input or decoded structure is too large,
    /// [`ValueWireDecodeError::UnsupportedVersion`] when the envelope declares
    /// another supported-width version, or
    /// [`ValueWireDecodeError::Syntax`] when the input is not one valid JSON
    /// document, or [`ValueWireDecodeError::InvalidJson`] when valid JSON
    /// cannot be decoded as a V1 wire value.
    #[cfg(feature = "json")]
    #[inline(always)]
    pub fn decode_json_slice(input: &[u8]) -> Result<Self, ValueWireDecodeError> {
        Self::decode_json_slice_with_limits(input, Self::default_json_decode_limits())
    }

    /// Decodes a V1 JSON wire value using explicit structural limits.
    ///
    /// The complete input length and decoded structure are checked before the
    /// value is returned. Embedded values should be checked through the outer
    /// protocol's shared [`qubit_budget::json::JsonDecodeSession`].
    ///
    /// # Parameters
    ///
    /// * `input` - Complete UTF-8 JSON document to decode.
    /// * `limits` - Shared encoded-input and structural limits.
    ///
    /// # Returns
    ///
    /// The decoded V1 wire DTO.
    ///
    /// # Errors
    ///
    /// Returns a limit error when `input` or its decoded structure exceeds
    /// `limits`, [`ValueWireDecodeError::UnsupportedVersion`] when the envelope
    /// declares another supported-width version,
    /// [`ValueWireDecodeError::Syntax`] when the input is not one valid JSON
    /// document, or [`ValueWireDecodeError::InvalidJson`] when valid JSON
    /// cannot be decoded as a V1 wire value.
    #[cfg(feature = "json")]
    #[inline]
    pub fn decode_json_slice_with_limits(input: &[u8], limits: JsonDecodeLimits) -> Result<Self, ValueWireDecodeError> {
        let session = JsonDecodeSession::from_limits(limits);
        super::decode_wire_json_slice_with_session(input, session)
    }

    /// Encodes this V1 document into a compact JSON vector with default limits.
    ///
    /// # Returns
    ///
    /// The encoded complete V1 document.
    ///
    /// # Errors
    ///
    /// Returns [`ValueWireEncodeError::Budget`] when the document exceeds the
    /// default JSON resource profile.
    #[cfg(feature = "json")]
    #[inline(always)]
    pub fn to_json_vec(&self) -> Result<Vec<u8>, ValueWireEncodeError> {
        self.to_json_vec_with_limits(Self::default_json_encode_limits())
    }

    /// Encodes this V1 document into a bounded compact JSON vector.
    ///
    /// # Parameters
    ///
    /// * `limits` - Resource limits enforced during encoding.
    ///
    /// # Returns
    ///
    /// Compact UTF-8 JSON bytes for the complete V1 document.
    ///
    /// # Errors
    ///
    /// Returns [`ValueWireEncodeError`] when encoding exceeds `limits` or the
    /// document cannot be serialized.
    #[cfg(feature = "json")]
    pub fn to_json_vec_with_limits(&self, limits: JsonEncodeLimits) -> Result<Vec<u8>, ValueWireEncodeError> {
        let session = JsonEncodeSession::from_limits(limits);
        JsonEncoder::new(session)
            .to_vec(self)
            .map_err(ValueWireEncodeError::from)
    }

    /// Encodes this V1 document to a writer with default limits.
    ///
    /// # Type Parameters
    ///
    /// * `W` - Destination writer type.
    ///
    /// # Parameters
    ///
    /// * `writer` - Destination receiving the complete JSON document.
    ///
    /// # Returns
    ///
    /// `Ok(())` after the complete document is written.
    ///
    /// # Errors
    ///
    /// Returns [`ValueWireEncodeError::Budget`] for resource-limit failures or
    /// [`ValueWireEncodeError::Io`] when `writer` rejects output.
    #[cfg(feature = "json")]
    #[inline(always)]
    pub fn to_json_writer<W>(&self, writer: W) -> Result<(), ValueWireEncodeError>
    where
        W: Write,
    {
        self.to_json_writer_with_limits(writer, Self::default_json_encode_limits())
    }

    /// Encodes this V1 document to a writer after enforcing JSON budgets.
    ///
    /// # Type Parameters
    ///
    /// * `W` - Destination writer type.
    ///
    /// # Parameters
    ///
    /// * `writer` - Destination receiving the complete JSON document.
    /// * `limits` - Resource limits enforced during encoding.
    ///
    /// # Returns
    ///
    /// `Ok(())` after the complete document is written.
    ///
    /// # Errors
    ///
    /// Returns [`ValueWireEncodeError`] when encoding exceeds `limits`, the
    /// document cannot be serialized, or `writer` rejects output.
    #[cfg(feature = "json")]
    pub fn to_json_writer_with_limits<W>(&self, writer: W, limits: JsonEncodeLimits) -> Result<(), ValueWireEncodeError>
    where
        W: Write,
    {
        let session = JsonEncodeSession::from_limits(limits);
        JsonEncoder::new(session)
            .write_buffered(writer, self)
            .map_err(ValueWireEncodeError::from)
    }

    /// Returns the runtime container represented by this DTO.
    ///
    /// # Returns
    ///
    /// A shared reference to the preserved runtime container.
    #[must_use = "the borrowed value container should be used"]
    #[inline(always)]
    pub const fn container(&self) -> &ValueContainer {
        self.value.container()
    }

    /// Consumes the DTO and returns its runtime container.
    ///
    /// # Returns
    ///
    /// The preserved runtime container.
    #[inline(always)]
    pub fn into_container(self) -> ValueContainer {
        self.value.into_container()
    }
}

impl TryFrom<Value> for ValueWireV1 {
    type Error = ValueWireEncodeError;
    /// Wraps a runtime scalar in a V1 DTO.
    #[inline(always)]
    fn try_from(value: Value) -> Result<Self, Self::Error> {
        ValueWirePayloadV1::try_from(value).map(Self::new)
    }
}

impl TryFrom<MultiValues> for ValueWireV1 {
    type Error = ValueWireEncodeError;
    /// Wraps a runtime collection in a V1 DTO.
    #[inline(always)]
    fn try_from(values: MultiValues) -> Result<Self, Self::Error> {
        ValueWirePayloadV1::try_from(values).map(Self::new)
    }
}

impl TryFrom<ValueContainer> for ValueWireV1 {
    type Error = ValueWireEncodeError;
    /// Wraps an explicit runtime shape in a V1 DTO.
    #[inline(always)]
    fn try_from(value: ValueContainer) -> Result<Self, Self::Error> {
        ValueWirePayloadV1::try_from(value).map(Self::new)
    }
}

impl From<ValueWireV1> for ValueContainer {
    /// Unwraps the runtime container from a V1 DTO.
    #[inline(always)]
    fn from(value: ValueWireV1) -> Self {
        value.into_container()
    }
}

impl Serialize for ValueWireV1 {
    /// Serializes the contained runtime shape through the V1 contract.
    #[inline(always)]
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serialize_wire(self.value.container().into(), serializer)
    }
}