Skip to main content

qubit_value/value_wire/
value_wire_v1.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
9//! Public DTO for the stable version-one JSON wire contract.
10
11#[cfg(feature = "json")]
12use std::io::Write;
13
14#[cfg(feature = "json")]
15use qubit_budget::json::JsonDecodeLimits;
16#[cfg(feature = "json")]
17use qubit_budget::json::JsonDecodeSession;
18#[cfg(feature = "json")]
19use qubit_budget::json::JsonEncodeLimits;
20#[cfg(feature = "json")]
21use qubit_budget::json::JsonEncodeSession;
22#[cfg(feature = "json")]
23use qubit_json::encode::JsonEncoder;
24use serde::Serialize;
25use serde::Serializer;
26
27use super::VALUE_WIRE_V1_VERSION;
28#[cfg(feature = "json")]
29use super::ValueWireDecodeError;
30use super::ValueWireEncodeError;
31use super::ValueWirePayloadV1;
32use super::serialize_wire;
33use crate::MultiValues;
34use crate::Value;
35use crate::ValueContainer;
36
37/// Stable version-one wire DTO for a scalar or homogeneous collection.
38///
39/// For a given serializer and value, output is byte-stable with canonical field
40/// and object-key order, including recursively nested JSON objects. V1 is
41/// closed: existing tags, shapes, and payload representations cannot change,
42/// and future runtime data types require a new wire version instead of
43/// extending V1.
44///
45/// Deserialization is intentionally available through
46/// [`crate::ValueWireV1Seed`], which lets a bounded decoder control the
47/// complete input and structure.
48///
49/// # Examples
50///
51/// ```
52/// use std::convert::TryFrom;
53/// use qubit_value::{Value, ValueWireV1};
54///
55/// let _wire = ValueWireV1::try_from(Value::from(42_i32)).unwrap();
56/// ```
57#[must_use]
58#[derive(Debug, Clone, PartialEq, Eq, Hash)]
59pub struct ValueWireV1 {
60    /// Explicit runtime shape and typed payload represented by this DTO.
61    value: ValueWirePayloadV1,
62}
63
64impl ValueWireV1 {
65    /// Numeric version emitted and accepted by this DTO.
66    pub const VERSION: u8 = VALUE_WIRE_V1_VERSION;
67
68    /// Creates a V1 DTO from an explicit scalar-or-collection container.
69    ///
70    /// # Parameters
71    ///
72    /// * `value` - Runtime container whose exact type and shape are preserved.
73    ///
74    /// # Returns
75    ///
76    /// A V1 DTO containing `value`.
77    #[inline(always)]
78    pub const fn new(value: ValueWirePayloadV1) -> Self {
79        Self { value }
80    }
81
82    /// Returns the default JSON resource profile for complete V1 documents.
83    ///
84    /// # Returns
85    ///
86    /// Decode limits suitable for one standalone V1 envelope.
87    #[cfg(feature = "json")]
88    #[must_use = "the V1 JSON profile should be applied to a budget"]
89    #[inline(always)]
90    pub fn default_json_decode_limits() -> JsonDecodeLimits {
91        super::default_json_decode_limits()
92    }
93
94    /// Returns the default JSON resource profile for encoding V1 documents.
95    ///
96    /// # Returns
97    ///
98    /// Encode limits suitable for one standalone V1 envelope.
99    #[cfg(feature = "json")]
100    #[must_use = "the V1 JSON profile should be applied to an encode session"]
101    #[inline(always)]
102    pub fn default_json_encode_limits() -> JsonEncodeLimits {
103        super::default_json_encode_limits()
104    }
105
106    /// Decodes a V1 JSON wire value using the default structural limits.
107    ///
108    /// The complete input length and decoded structure are checked before the
109    /// value is returned. Embedded protocols should share one
110    /// [`qubit_budget::json::JsonDecodeSession`] across their complete
111    /// document.
112    ///
113    /// # Parameters
114    ///
115    /// * `input` - Complete UTF-8 JSON document to decode.
116    ///
117    /// # Returns
118    ///
119    /// The decoded V1 wire DTO.
120    ///
121    /// # Errors
122    ///
123    /// Returns a limit error when the input or decoded structure is too large,
124    /// [`ValueWireDecodeError::UnsupportedVersion`] when the envelope declares
125    /// another supported-width version, or
126    /// [`ValueWireDecodeError::Syntax`] when the input is not one valid JSON
127    /// document, or [`ValueWireDecodeError::InvalidJson`] when valid JSON
128    /// cannot be decoded as a V1 wire value.
129    #[cfg(feature = "json")]
130    #[inline(always)]
131    pub fn decode_json_slice(input: &[u8]) -> Result<Self, ValueWireDecodeError> {
132        Self::decode_json_slice_with_limits(input, Self::default_json_decode_limits())
133    }
134
135    /// Decodes a V1 JSON wire value using explicit structural limits.
136    ///
137    /// The complete input length and decoded structure are checked before the
138    /// value is returned. Embedded values should be checked through the outer
139    /// protocol's shared [`qubit_budget::json::JsonDecodeSession`].
140    ///
141    /// # Parameters
142    ///
143    /// * `input` - Complete UTF-8 JSON document to decode.
144    /// * `limits` - Shared encoded-input and structural limits.
145    ///
146    /// # Returns
147    ///
148    /// The decoded V1 wire DTO.
149    ///
150    /// # Errors
151    ///
152    /// Returns a limit error when `input` or its decoded structure exceeds
153    /// `limits`, [`ValueWireDecodeError::UnsupportedVersion`] when the envelope
154    /// declares another supported-width version,
155    /// [`ValueWireDecodeError::Syntax`] when the input is not one valid JSON
156    /// document, or [`ValueWireDecodeError::InvalidJson`] when valid JSON
157    /// cannot be decoded as a V1 wire value.
158    #[cfg(feature = "json")]
159    #[inline]
160    pub fn decode_json_slice_with_limits(input: &[u8], limits: JsonDecodeLimits) -> Result<Self, ValueWireDecodeError> {
161        let session = JsonDecodeSession::from_limits(limits);
162        super::decode_wire_json_slice_with_session(input, session)
163    }
164
165    /// Encodes this V1 document into a compact JSON vector with default limits.
166    ///
167    /// # Returns
168    ///
169    /// The encoded complete V1 document.
170    ///
171    /// # Errors
172    ///
173    /// Returns [`ValueWireEncodeError::Budget`] when the document exceeds the
174    /// default JSON resource profile.
175    #[cfg(feature = "json")]
176    #[inline(always)]
177    pub fn to_json_vec(&self) -> Result<Vec<u8>, ValueWireEncodeError> {
178        self.to_json_vec_with_limits(Self::default_json_encode_limits())
179    }
180
181    /// Encodes this V1 document into a bounded compact JSON vector.
182    ///
183    /// # Parameters
184    ///
185    /// * `limits` - Resource limits enforced during encoding.
186    ///
187    /// # Returns
188    ///
189    /// Compact UTF-8 JSON bytes for the complete V1 document.
190    ///
191    /// # Errors
192    ///
193    /// Returns [`ValueWireEncodeError`] when encoding exceeds `limits` or the
194    /// document cannot be serialized.
195    #[cfg(feature = "json")]
196    pub fn to_json_vec_with_limits(&self, limits: JsonEncodeLimits) -> Result<Vec<u8>, ValueWireEncodeError> {
197        let session = JsonEncodeSession::from_limits(limits);
198        JsonEncoder::new(session)
199            .to_vec(self)
200            .map_err(ValueWireEncodeError::from)
201    }
202
203    /// Encodes this V1 document to a writer with default limits.
204    ///
205    /// # Type Parameters
206    ///
207    /// * `W` - Destination writer type.
208    ///
209    /// # Parameters
210    ///
211    /// * `writer` - Destination receiving the complete JSON document.
212    ///
213    /// # Returns
214    ///
215    /// `Ok(())` after the complete document is written.
216    ///
217    /// # Errors
218    ///
219    /// Returns [`ValueWireEncodeError::Budget`] for resource-limit failures or
220    /// [`ValueWireEncodeError::Io`] when `writer` rejects output.
221    #[cfg(feature = "json")]
222    #[inline(always)]
223    pub fn to_json_writer<W>(&self, writer: W) -> Result<(), ValueWireEncodeError>
224    where
225        W: Write,
226    {
227        self.to_json_writer_with_limits(writer, Self::default_json_encode_limits())
228    }
229
230    /// Encodes this V1 document to a writer after enforcing JSON budgets.
231    ///
232    /// # Type Parameters
233    ///
234    /// * `W` - Destination writer type.
235    ///
236    /// # Parameters
237    ///
238    /// * `writer` - Destination receiving the complete JSON document.
239    /// * `limits` - Resource limits enforced during encoding.
240    ///
241    /// # Returns
242    ///
243    /// `Ok(())` after the complete document is written.
244    ///
245    /// # Errors
246    ///
247    /// Returns [`ValueWireEncodeError`] when encoding exceeds `limits`, the
248    /// document cannot be serialized, or `writer` rejects output.
249    #[cfg(feature = "json")]
250    pub fn to_json_writer_with_limits<W>(&self, writer: W, limits: JsonEncodeLimits) -> Result<(), ValueWireEncodeError>
251    where
252        W: Write,
253    {
254        let session = JsonEncodeSession::from_limits(limits);
255        JsonEncoder::new(session)
256            .write_buffered(writer, self)
257            .map_err(ValueWireEncodeError::from)
258    }
259
260    /// Returns the runtime container represented by this DTO.
261    ///
262    /// # Returns
263    ///
264    /// A shared reference to the preserved runtime container.
265    #[must_use = "the borrowed value container should be used"]
266    #[inline(always)]
267    pub const fn container(&self) -> &ValueContainer {
268        self.value.container()
269    }
270
271    /// Consumes the DTO and returns its runtime container.
272    ///
273    /// # Returns
274    ///
275    /// The preserved runtime container.
276    #[inline(always)]
277    pub fn into_container(self) -> ValueContainer {
278        self.value.into_container()
279    }
280}
281
282impl TryFrom<Value> for ValueWireV1 {
283    type Error = ValueWireEncodeError;
284    /// Wraps a runtime scalar in a V1 DTO.
285    #[inline(always)]
286    fn try_from(value: Value) -> Result<Self, Self::Error> {
287        ValueWirePayloadV1::try_from(value).map(Self::new)
288    }
289}
290
291impl TryFrom<MultiValues> for ValueWireV1 {
292    type Error = ValueWireEncodeError;
293    /// Wraps a runtime collection in a V1 DTO.
294    #[inline(always)]
295    fn try_from(values: MultiValues) -> Result<Self, Self::Error> {
296        ValueWirePayloadV1::try_from(values).map(Self::new)
297    }
298}
299
300impl TryFrom<ValueContainer> for ValueWireV1 {
301    type Error = ValueWireEncodeError;
302    /// Wraps an explicit runtime shape in a V1 DTO.
303    #[inline(always)]
304    fn try_from(value: ValueContainer) -> Result<Self, Self::Error> {
305        ValueWirePayloadV1::try_from(value).map(Self::new)
306    }
307}
308
309impl From<ValueWireV1> for ValueContainer {
310    /// Unwraps the runtime container from a V1 DTO.
311    #[inline(always)]
312    fn from(value: ValueWireV1) -> Self {
313        value.into_container()
314    }
315}
316
317impl Serialize for ValueWireV1 {
318    /// Serializes the contained runtime shape through the V1 contract.
319    #[inline(always)]
320    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
321    where
322        S: Serializer,
323    {
324        serialize_wire(self.value.container().into(), serializer)
325    }
326}