qubit-value 0.12.2

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.
// =============================================================================

//! Unversioned V1 payload for use inside an already-versioned protocol.

#[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::decode::JsonDecoder;
#[cfg(feature = "json")]
use qubit_json::encode::JsonEncoder;
use serde::Serialize;
use serde::Serializer;

use super::ValueWireEncodeError;
use super::WireShapeRef;
use super::value_wire_payload_ref_v1::validate_value;
use super::value_wire_payload_ref_v1::validate_values;
#[cfg(feature = "json")]
use super::value_wire_payload_v1_seed::ValueWirePayloadV1Seed;
use crate::MultiValues;
use crate::Value;
use crate::ValueContainer;
#[cfg(feature = "json")]
use crate::ValueWireDecodeError;

/// Typed V1 scalar-or-collection payload without an enclosing version field.
///
/// Deserialization is intentionally available through
/// [`crate::ValueWirePayloadV1Seed`], which lets a bounded decoder control the
/// complete input and structure.
///
/// # Examples
///
/// ```
/// use std::convert::TryFrom;
/// use qubit_value::{Value, ValueWirePayloadV1};
///
/// let _payload = ValueWirePayloadV1::try_from(Value::from(42_i32)).unwrap();
/// ```
#[must_use]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ValueWirePayloadV1 {
    /// Preserved runtime shape and payload.
    value: ValueContainer,
}

impl ValueWirePayloadV1 {
    /// Wraps a payload decoded through V1's finite-number Serde adapters.
    ///
    /// # Parameters
    ///
    /// * `value` - Already validated decoded runtime container.
    ///
    /// # Returns
    ///
    /// An owned unversioned V1 payload.
    pub(in crate::value_wire) const fn from_decoded(value: ValueContainer) -> Self {
        Self { value }
    }

    /// Builds a payload after enforcing V1's finite-float invariant.
    ///
    /// # Parameters
    ///
    /// * `value` - Runtime container to validate and own.
    ///
    /// # Returns
    ///
    /// A validated owned V1 payload.
    ///
    /// # Errors
    ///
    /// Returns [`ValueWireEncodeError`] when any contained numeric payload is
    /// not representable by V1.
    fn try_new(value: ValueContainer) -> Result<Self, ValueWireEncodeError> {
        match &value {
            ValueContainer::Scalar(value) => validate_value(value)?,
            ValueContainer::Collection(values) => validate_values(values)?,
        }
        Ok(Self { value })
    }

    /// Returns the default JSON resource profile for complete V1 payloads.
    ///
    /// # Returns
    ///
    /// Decode limits suitable for one standalone unversioned V1 payload.
    #[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 complete V1 payloads.
    ///
    /// # Returns
    ///
    /// Encode limits suitable for one standalone unversioned V1 payload.
    #[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 complete V1 JSON payload using default resource limits.
    ///
    /// Prefer this entry point when the payload itself is the complete
    /// untrusted document. Embedded protocols should share one budget across
    /// all payloads in their complete document.
    ///
    /// # Parameters
    ///
    /// * `input` - Complete UTF-8 JSON payload to decode.
    ///
    /// # Returns
    ///
    /// The decoded unversioned V1 payload.
    ///
    /// # Errors
    ///
    /// Returns a limit error when the input or decoded structure is too large,
    /// or [`ValueWireDecodeError::InvalidJson`] for malformed input.
    #[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 complete V1 JSON payload using explicit resource limits.
    ///
    /// # Parameters
    ///
    /// * `input` - Complete UTF-8 JSON payload to decode.
    /// * `limits` - Resource limits enforced during decoding.
    ///
    /// # Returns
    ///
    /// The decoded unversioned V1 payload.
    ///
    /// # Errors
    ///
    /// Returns a limit error when `input` or its decoded structure exceeds
    /// `limits`, or [`ValueWireDecodeError::InvalidJson`] for malformed input.
    #[cfg(feature = "json")]
    #[inline]
    pub fn decode_json_slice_with_limits(input: &[u8], limits: JsonDecodeLimits) -> Result<Self, ValueWireDecodeError> {
        let session = JsonDecodeSession::from_limits(limits);
        JsonDecoder::new(session)
            .decode_seed_utf8(ValueWirePayloadV1Seed::new(), input)
            .map_err(ValueWireDecodeError::from)
    }

    /// Encodes this V1 payload into a compact JSON vector with default limits.
    ///
    /// # Returns
    ///
    /// Compact UTF-8 JSON bytes for this unversioned payload.
    ///
    /// # Errors
    ///
    /// Returns [`ValueWireEncodeError::Budget`] when the payload 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 payload into a bounded compact JSON vector.
    ///
    /// # Parameters
    ///
    /// * `limits` - Resource limits enforced during encoding.
    ///
    /// # Returns
    ///
    /// Compact UTF-8 JSON bytes for this unversioned payload.
    ///
    /// # Errors
    ///
    /// Returns [`ValueWireEncodeError`] when encoding exceeds `limits` or the
    /// payload 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 payload to a writer with default limits.
    ///
    /// # Type Parameters
    ///
    /// * `W` - Destination writer type.
    ///
    /// # Parameters
    ///
    /// * `writer` - Destination receiving the complete JSON payload.
    ///
    /// # Returns
    ///
    /// `Ok(())` after the complete payload 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 payload to a writer after enforcing JSON budgets.
    ///
    /// # Type Parameters
    ///
    /// * `W` - Destination writer type.
    ///
    /// # Parameters
    ///
    /// * `writer` - Destination receiving the complete JSON payload.
    /// * `limits` - Resource limits enforced during encoding.
    ///
    /// # Returns
    ///
    /// `Ok(())` after the complete payload is written.
    ///
    /// # Errors
    ///
    /// Returns [`ValueWireEncodeError`] when encoding exceeds `limits`, the
    /// payload 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)
    }

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

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

impl TryFrom<Value> for ValueWirePayloadV1 {
    type Error = ValueWireEncodeError;

    /// Validates a scalar for use in a V1 payload.
    #[inline(always)]
    fn try_from(value: Value) -> Result<Self, Self::Error> {
        Self::try_new(value.into())
    }
}

impl TryFrom<MultiValues> for ValueWirePayloadV1 {
    type Error = ValueWireEncodeError;

    /// Validates a collection for use in a V1 payload.
    #[inline(always)]
    fn try_from(value: MultiValues) -> Result<Self, Self::Error> {
        Self::try_new(value.into())
    }
}

impl TryFrom<ValueContainer> for ValueWirePayloadV1 {
    type Error = ValueWireEncodeError;

    /// Validates an explicitly shaped value for use in a V1 payload.
    #[inline(always)]
    fn try_from(value: ValueContainer) -> Result<Self, Self::Error> {
        Self::try_new(value)
    }
}

impl From<ValueWirePayloadV1> for ValueContainer {
    /// Extracts the shaped runtime value from a V1 payload.
    #[inline(always)]
    fn from(value: ValueWirePayloadV1) -> Self {
        value.into_container()
    }
}

impl Serialize for ValueWirePayloadV1 {
    /// Serializes the unversioned V1 shape.
    #[inline(always)]
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        WireShapeRef::from(&self.value).serialize(serializer)
    }
}