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;
31#[cfg(feature = "json")]
32use super::ValueWireEncodePreflight;
33use super::ValueWirePayloadV1;
34use super::serialize_wire;
35use crate::MultiValues;
36use crate::Value;
37use crate::ValueContainer;
38
39/// Stable version-one wire DTO for a scalar or homogeneous collection.
40///
41/// Canonical JSON output is byte-stable for the same value under the
42/// supported `serde_json` version and configuration, with canonical field
43/// and object-key order, including recursively nested JSON objects. Other
44/// Serde formats may represent this DTO, but their bytes are outside this
45/// stability contract. See the [crate documentation](crate).
46///
47/// V1 is closed: existing tags, shapes, and payload representations cannot
48/// change, and future runtime data types require a new wire version instead
49/// of extending V1.
50///
51/// Runtime JSON numbers are accepted only when V1 can reconstruct the same
52/// `serde_json::Number` under the strict `qubit-json` number model. Numbers
53/// that would change identity when round-tripped return the JSON-gated
54/// `JsonNumberNotRoundTrippable` error. This error does not expose the number
55/// text.
56///
57/// Deserialization is intentionally available through
58/// [`crate::ValueWireV1Seed`], which lets a bounded decoder control the
59/// complete input and structure.
60///
61/// # Examples
62///
63/// ```
64/// use qubit_value::{Value, ValueWireV1};
65///
66/// let wire = ValueWireV1::try_from(Value::from(42_i32)).unwrap();
67/// let encoded = serde_json::to_string(&wire).unwrap();
68/// assert_eq!(encoded, r#"{"version":1,"value":{"scalar":{"int32":42}}}"#);
69/// assert_eq!(wire.into_container().get_first::<i32>().unwrap(), 42);
70/// ```
71#[must_use]
72#[derive(Debug, Clone, PartialEq, Eq, Hash)]
73pub struct ValueWireV1 {
74    /// Explicit runtime shape and typed payload represented by this DTO.
75    value: ValueWirePayloadV1,
76}
77
78impl ValueWireV1 {
79    /// Numeric version emitted and accepted by this DTO.
80    pub const VERSION: u8 = VALUE_WIRE_V1_VERSION;
81
82    /// Creates a V1 DTO from an already validated V1 payload.
83    ///
84    /// # Parameters
85    ///
86    /// * `value` - Validated [`ValueWirePayloadV1`] moved into the envelope,
87    ///   preserving its exact runtime type and scalar-or-collection shape.
88    ///
89    /// # Returns
90    ///
91    /// A V1 DTO containing `value`.
92    #[inline(always)]
93    pub const fn new(value: ValueWirePayloadV1) -> Self {
94        Self { value }
95    }
96
97    /// Decodes a V1 JSON wire value using the default structural limits.
98    ///
99    /// The complete input length and decoded structure are checked before the
100    /// value is returned. Embedded protocols should share one
101    /// [`qubit_budget::json::JsonDecodeSession`] across their complete
102    /// document.
103    ///
104    /// # Parameters
105    ///
106    /// * `input` - Complete UTF-8 JSON document to decode.
107    ///
108    /// # Returns
109    ///
110    /// The decoded V1 wire DTO.
111    ///
112    /// # Errors
113    ///
114    /// Returns a limit error when the input or decoded structure is too large,
115    /// [`ValueWireDecodeError::UnsupportedVersion`] when the envelope declares
116    /// another supported-width version, or
117    /// [`ValueWireDecodeError::Syntax`] when the input is not one valid JSON
118    /// document, or [`ValueWireDecodeError::InvalidJson`] when valid JSON
119    /// cannot be decoded as a V1 wire value.
120    #[cfg(feature = "json")]
121    #[inline]
122    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
123    pub fn decode_json_slice(input: &[u8]) -> Result<Self, ValueWireDecodeError> {
124        Self::decode_json_slice_with_limits(input, Self::default_json_decode_limits())
125    }
126
127    /// Decodes a V1 JSON wire value using explicit structural limits.
128    ///
129    /// The complete input length and decoded structure are checked before the
130    /// value is returned. Embedded values should be checked through the outer
131    /// protocol's shared [`qubit_budget::json::JsonDecodeSession`].
132    ///
133    /// # Parameters
134    ///
135    /// * `input` - Complete UTF-8 JSON document to decode.
136    /// * `limits` - Shared encoded-input and structural limits.
137    ///
138    /// # Returns
139    ///
140    /// The decoded V1 wire DTO.
141    ///
142    /// # Errors
143    ///
144    /// Returns a limit error when `input` or its decoded structure exceeds
145    /// `limits`, [`ValueWireDecodeError::UnsupportedVersion`] when the envelope
146    /// declares another supported-width version,
147    /// [`ValueWireDecodeError::Syntax`] when the input is not one valid JSON
148    /// document, or [`ValueWireDecodeError::InvalidJson`] when valid JSON
149    /// cannot be decoded as a V1 wire value.
150    #[cfg(feature = "json")]
151    #[inline]
152    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
153    pub fn decode_json_slice_with_limits(
154        input: &[u8],
155        limits: JsonDecodeLimits,
156    ) -> Result<Self, ValueWireDecodeError> {
157        let session = JsonDecodeSession::from_limits(limits);
158        super::decode_wire_json_slice_with_session(input, session)
159    }
160
161    /// Returns the default JSON resource profile for complete V1 documents.
162    ///
163    /// # Returns
164    ///
165    /// Decode limits suitable for one standalone V1 envelope.
166    #[cfg(feature = "json")]
167    #[must_use = "the V1 JSON profile should be applied to a budget"]
168    #[inline]
169    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
170    pub fn default_json_decode_limits() -> JsonDecodeLimits {
171        super::default_json_decode_limits()
172    }
173
174    /// Returns the default JSON resource profile for encoding V1 documents.
175    ///
176    /// # Returns
177    ///
178    /// Encode limits suitable for one standalone V1 envelope.
179    #[cfg(feature = "json")]
180    #[must_use = "the V1 JSON profile should be applied to an encode session"]
181    #[inline(always)]
182    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
183    pub fn default_json_encode_limits() -> JsonEncodeLimits {
184        super::default_json_encode_limits()
185    }
186
187    /// Returns the runtime container represented by this DTO.
188    ///
189    /// # Returns
190    ///
191    /// A shared reference to the preserved runtime container.
192    #[must_use = "the borrowed value container should be used"]
193    #[inline]
194    pub const fn container(&self) -> &ValueContainer {
195        self.value.container()
196    }
197
198    /// Encodes this V1 document into a compact JSON vector with default limits.
199    ///
200    /// # Returns
201    ///
202    /// The encoded complete V1 document.
203    ///
204    /// # Errors
205    ///
206    /// Returns [`ValueWireEncodeError::Budget`] when the document exceeds the
207    /// default JSON resource profile.
208    #[cfg(feature = "json")]
209    #[inline]
210    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
211    pub fn to_json_vec(&self) -> Result<Vec<u8>, ValueWireEncodeError> {
212        self.to_json_vec_with_limits(Self::default_json_encode_limits())
213    }
214
215    /// Encodes this V1 document into a bounded compact JSON vector.
216    ///
217    /// # Parameters
218    ///
219    /// * `limits` - Resource limits enforced during encoding.
220    ///
221    /// # Returns
222    ///
223    /// Compact UTF-8 JSON bytes for the complete V1 document.
224    ///
225    /// Runs a conservative payload preflight before encoding, then enforces
226    /// `limits` authoritatively while encoding the full envelope. The preflight
227    /// may report a lower-bound budget error and does not bound every
228    /// preparation allocation.
229    ///
230    /// # Errors
231    ///
232    /// Returns [`ValueWireEncodeError`] when preflight or final encoding
233    /// exceeds `limits`, or the document cannot be serialized.
234    #[cfg(feature = "json")]
235    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
236    pub fn to_json_vec_with_limits(
237        &self,
238        limits: JsonEncodeLimits,
239    ) -> Result<Vec<u8>, ValueWireEncodeError> {
240        ValueWireEncodePreflight::new(limits)
241            .check_container(self.container())
242            .map_err(ValueWireEncodeError::from)?;
243        let session = JsonEncodeSession::from_limits(limits);
244        JsonEncoder::new(session)
245            .to_vec(self)
246            .map_err(ValueWireEncodeError::from)
247    }
248
249    /// Encodes this V1 document to a writer with default limits.
250    ///
251    /// # Type Parameters
252    ///
253    /// * `W` - Destination writer type.
254    ///
255    /// # Parameters
256    ///
257    /// * `writer` - Destination receiving the complete JSON document.
258    ///
259    /// # Returns
260    ///
261    /// `Ok(())` after the complete document is written.
262    ///
263    /// The complete document is buffered and admitted before this method writes
264    /// its bytes to the destination. Preflight, serialization, or budget
265    /// rejection does not write this document. An I/O error may leave an
266    /// accepted prefix in the writer; do not blindly retry a complete
267    /// document on the same destination.
268    ///
269    /// This method does not explicitly flush the writer. Success means the
270    /// writer accepted all document bytes, not that they were flushed,
271    /// persisted, or received by a remote peer. Pass `&mut writer` to
272    /// retain ownership of a buffered writer, then call `flush()` and
273    /// handle its error separately. Passing an owned writer may invoke that
274    /// writer's drop behavior; errors from an implicit drop-time flush are
275    /// not returned by this method.
276    ///
277    /// # Errors
278    ///
279    /// Returns [`ValueWireEncodeError::Budget`] for resource-limit failures or
280    /// [`ValueWireEncodeError::Io`] when `writer` rejects output.
281    #[cfg(feature = "json")]
282    #[inline(always)]
283    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
284    pub fn to_json_writer<W>(&self, writer: W) -> Result<(), ValueWireEncodeError>
285    where
286        W: Write,
287    {
288        self.to_json_writer_with_limits(writer, Self::default_json_encode_limits())
289    }
290
291    /// Encodes this V1 document to a writer after enforcing JSON budgets.
292    ///
293    /// # Type Parameters
294    ///
295    /// * `W` - Destination writer type.
296    ///
297    /// # Parameters
298    ///
299    /// * `writer` - Destination receiving the complete JSON document.
300    /// * `limits` - Resource limits enforced during encoding.
301    ///
302    /// # Returns
303    ///
304    /// `Ok(())` after the complete document is written.
305    ///
306    /// Runs a conservative payload preflight before encoding, then enforces
307    /// `limits` authoritatively while encoding the full envelope. The preflight
308    /// may report a lower-bound budget error and does not bound every
309    /// preparation allocation.
310    ///
311    /// The complete document is buffered and admitted before this method writes
312    /// its bytes to the destination. Preflight, serialization, or budget
313    /// rejection does not write this document. An I/O error may leave an
314    /// accepted prefix in the writer; do not blindly retry a complete
315    /// document on the same destination.
316    ///
317    /// This method does not explicitly flush the writer. Success means the
318    /// writer accepted all document bytes, not that they were flushed,
319    /// persisted, or received by a remote peer. Pass `&mut writer` to
320    /// retain ownership of a buffered writer, then call `flush()` and
321    /// handle its error separately. Passing an owned writer may invoke that
322    /// writer's drop behavior; errors from an implicit drop-time flush are
323    /// not returned by this method.
324    ///
325    /// # Errors
326    ///
327    /// Returns [`ValueWireEncodeError`] when preflight or final encoding
328    /// exceeds `limits`, the document cannot be serialized, or `writer`
329    /// rejects output.
330    #[cfg(feature = "json")]
331    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
332    pub fn to_json_writer_with_limits<W>(
333        &self,
334        mut writer: W,
335        limits: JsonEncodeLimits,
336    ) -> Result<(), ValueWireEncodeError>
337    where
338        W: Write,
339    {
340        ValueWireEncodePreflight::new(limits)
341            .check_container(self.container())
342            .map_err(ValueWireEncodeError::from)?;
343        let session = JsonEncodeSession::from_limits(limits);
344        JsonEncoder::new(session)
345            .write_buffered(&mut writer, self)
346            .map_err(ValueWireEncodeError::from)
347    }
348
349    /// Consumes the DTO and returns its runtime container.
350    ///
351    /// # Returns
352    ///
353    /// The preserved runtime container.
354    #[inline(always)]
355    pub fn into_container(self) -> ValueContainer {
356        self.value.into_container()
357    }
358}
359
360impl TryFrom<Value> for ValueWireV1 {
361    /// Failure to represent the consumed runtime value under the V1 contract.
362    type Error = ValueWireEncodeError;
363
364    /// Wraps a runtime scalar in a V1 DTO.
365    ///
366    /// # Parameters
367    ///
368    /// * `value` - Runtime scalar to validate and consume.
369    ///
370    /// # Returns
371    ///
372    /// A V1 DTO preserving the input type and shape.
373    ///
374    /// # Errors
375    ///
376    /// Returns [`ValueWireEncodeError::NonFiniteFloat`] for a non-finite
377    /// float, [`ValueWireEncodeError::BigDecimalScaleTooLarge`] for a decimal
378    /// whose exponent is outside V1's supported range, or
379    /// [`ValueWireEncodeError::InstantYearOutOfRange`] for an instant outside
380    /// the supported UTC year range.
381    /// With the `json` feature, returns `JsonNumberNotRoundTrippable` if a
382    /// runtime JSON number, including a nested number, cannot preserve its
383    /// `serde_json::Number` identity under the V1 number model.
384    #[inline(always)]
385    fn try_from(value: Value) -> Result<Self, Self::Error> {
386        ValueWirePayloadV1::try_from(value).map(Self::new)
387    }
388}
389
390impl TryFrom<MultiValues> for ValueWireV1 {
391    /// Failure to represent the consumed runtime value under the V1 contract.
392    type Error = ValueWireEncodeError;
393
394    /// Wraps a runtime collection in a V1 DTO.
395    ///
396    /// # Parameters
397    ///
398    /// * `values` - Homogeneous collection to validate and consume.
399    ///
400    /// # Returns
401    ///
402    /// A V1 DTO preserving the input type and shape.
403    ///
404    /// # Errors
405    ///
406    /// Returns [`ValueWireEncodeError::NonFiniteFloat`] for a non-finite
407    /// float element, [`ValueWireEncodeError::BigDecimalScaleTooLarge`] for
408    /// an element whose decimal exponent is outside V1's supported range, or
409    /// [`ValueWireEncodeError::InstantYearOutOfRange`] for an instant outside
410    /// the supported UTC year range.
411    /// With the `json` feature, returns `JsonNumberNotRoundTrippable` if any
412    /// runtime JSON number in the collection, including a nested number,
413    /// cannot preserve its `serde_json::Number` identity under the V1 number
414    /// model.
415    #[inline(always)]
416    fn try_from(values: MultiValues) -> Result<Self, Self::Error> {
417        ValueWirePayloadV1::try_from(values).map(Self::new)
418    }
419}
420
421impl TryFrom<ValueContainer> for ValueWireV1 {
422    /// Failure to represent the consumed runtime value under the V1 contract.
423    type Error = ValueWireEncodeError;
424
425    /// Wraps an explicit runtime shape in a V1 DTO.
426    ///
427    /// # Parameters
428    ///
429    /// * `value` - Scalar-or-collection container to validate and consume.
430    ///
431    /// # Returns
432    ///
433    /// A V1 DTO preserving the input type and shape.
434    ///
435    /// # Errors
436    ///
437    /// Returns [`ValueWireEncodeError::NonFiniteFloat`] for a non-finite
438    /// float, [`ValueWireEncodeError::BigDecimalScaleTooLarge`] for a decimal
439    /// whose exponent is outside V1's supported range, or
440    /// [`ValueWireEncodeError::InstantYearOutOfRange`] for an instant outside
441    /// the supported UTC year range.
442    /// With the `json` feature, returns `JsonNumberNotRoundTrippable` if a
443    /// runtime JSON number in either the scalar or collection shape, including
444    /// a nested number, cannot preserve its `serde_json::Number` identity
445    /// under the V1 number model.
446    #[inline]
447    fn try_from(value: ValueContainer) -> Result<Self, Self::Error> {
448        ValueWirePayloadV1::try_from(value).map(Self::new)
449    }
450}
451
452impl From<ValueWireV1> for ValueContainer {
453    /// Unwraps the runtime container from a V1 DTO.
454    ///
455    /// # Parameters
456    ///
457    /// * `value` - Owned DTO whose container is moved into the result.
458    ///
459    /// # Returns
460    ///
461    /// The preserved runtime container without cloning its payload.
462    #[inline(always)]
463    fn from(value: ValueWireV1) -> Self {
464        value.into_container()
465    }
466}
467
468impl Serialize for ValueWireV1 {
469    /// Serializes the contained runtime shape through the V1 contract.
470    ///
471    /// # Type Parameters
472    ///
473    /// * `S` - Serde serializer defining the output format and error type.
474    ///
475    /// # Parameters
476    ///
477    /// * `serializer` - Serializer consumed to emit the versioned envelope.
478    ///
479    /// # Returns
480    ///
481    /// The serializer's completed output on success.
482    ///
483    /// # Errors
484    ///
485    /// Returns the serializer's error if it cannot encode the envelope or
486    /// payload.
487    #[inline(always)]
488    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
489    where
490        S: Serializer,
491    {
492        serialize_wire(self.value.container().into(), serializer)
493    }
494}