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(input: &[u8], limits: JsonDecodeLimits) -> Result<Self, ValueWireDecodeError> {
154        let session = JsonDecodeSession::from_limits(limits);
155        super::decode_wire_json_slice_with_session(input, session)
156    }
157
158    /// Returns the default JSON resource profile for complete V1 documents.
159    ///
160    /// # Returns
161    ///
162    /// Decode limits suitable for one standalone V1 envelope.
163    #[cfg(feature = "json")]
164    #[must_use = "the V1 JSON profile should be applied to a budget"]
165    #[inline]
166    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
167    pub fn default_json_decode_limits() -> JsonDecodeLimits {
168        super::default_json_decode_limits()
169    }
170
171    /// Returns the default JSON resource profile for encoding V1 documents.
172    ///
173    /// # Returns
174    ///
175    /// Encode limits suitable for one standalone V1 envelope.
176    #[cfg(feature = "json")]
177    #[must_use = "the V1 JSON profile should be applied to an encode session"]
178    #[inline(always)]
179    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
180    pub fn default_json_encode_limits() -> JsonEncodeLimits {
181        super::default_json_encode_limits()
182    }
183
184    /// Returns the runtime container represented by this DTO.
185    ///
186    /// # Returns
187    ///
188    /// A shared reference to the preserved runtime container.
189    #[must_use = "the borrowed value container should be used"]
190    #[inline]
191    pub const fn container(&self) -> &ValueContainer {
192        self.value.container()
193    }
194
195    /// Encodes this V1 document into a compact JSON vector with default limits.
196    ///
197    /// # Returns
198    ///
199    /// The encoded complete V1 document.
200    ///
201    /// # Errors
202    ///
203    /// Returns [`ValueWireEncodeError::Budget`] when the document exceeds the
204    /// default JSON resource profile.
205    #[cfg(feature = "json")]
206    #[inline]
207    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
208    pub fn to_json_vec(&self) -> Result<Vec<u8>, ValueWireEncodeError> {
209        self.to_json_vec_with_limits(Self::default_json_encode_limits())
210    }
211
212    /// Encodes this V1 document into a bounded compact JSON vector.
213    ///
214    /// # Parameters
215    ///
216    /// * `limits` - Resource limits enforced during encoding.
217    ///
218    /// # Returns
219    ///
220    /// Compact UTF-8 JSON bytes for the complete V1 document.
221    ///
222    /// Runs a conservative payload preflight before encoding, then enforces
223    /// `limits` authoritatively while encoding the full envelope. The preflight
224    /// may report a lower-bound budget error and does not bound every
225    /// preparation allocation.
226    ///
227    /// # Errors
228    ///
229    /// Returns [`ValueWireEncodeError`] when preflight or final encoding
230    /// exceeds `limits`, or the document cannot be serialized.
231    #[cfg(feature = "json")]
232    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
233    pub fn to_json_vec_with_limits(&self, limits: JsonEncodeLimits) -> Result<Vec<u8>, ValueWireEncodeError> {
234        ValueWireEncodePreflight::new(limits)
235            .check_container(self.container())
236            .map_err(ValueWireEncodeError::from)?;
237        let session = JsonEncodeSession::from_limits(limits);
238        JsonEncoder::new(session)
239            .to_vec(self)
240            .map_err(ValueWireEncodeError::from)
241    }
242
243    /// Encodes this V1 document to a writer with default limits.
244    ///
245    /// # Type Parameters
246    ///
247    /// * `W` - Destination writer type.
248    ///
249    /// # Parameters
250    ///
251    /// * `writer` - Destination receiving the complete JSON document.
252    ///
253    /// # Returns
254    ///
255    /// `Ok(())` after the complete document is written.
256    ///
257    /// The complete document is buffered and admitted before this method writes
258    /// its bytes to the destination. Preflight, serialization, or budget
259    /// rejection does not write this document. An I/O error may leave an
260    /// accepted prefix in the writer; do not blindly retry a complete
261    /// document on the same destination.
262    ///
263    /// This method does not explicitly flush the writer. Success means the
264    /// writer accepted all document bytes, not that they were flushed,
265    /// persisted, or received by a remote peer. Pass `&mut writer` to
266    /// retain ownership of a buffered writer, then call `flush()` and
267    /// handle its error separately. Passing an owned writer may invoke that
268    /// writer's drop behavior; errors from an implicit drop-time flush are
269    /// not returned by this method.
270    ///
271    /// # Errors
272    ///
273    /// Returns [`ValueWireEncodeError::Budget`] for resource-limit failures or
274    /// [`ValueWireEncodeError::Io`] when `writer` rejects output.
275    #[cfg(feature = "json")]
276    #[inline(always)]
277    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
278    pub fn to_json_writer<W>(&self, writer: W) -> Result<(), ValueWireEncodeError>
279    where
280        W: Write,
281    {
282        self.to_json_writer_with_limits(writer, Self::default_json_encode_limits())
283    }
284
285    /// Encodes this V1 document to a writer after enforcing JSON budgets.
286    ///
287    /// # Type Parameters
288    ///
289    /// * `W` - Destination writer type.
290    ///
291    /// # Parameters
292    ///
293    /// * `writer` - Destination receiving the complete JSON document.
294    /// * `limits` - Resource limits enforced during encoding.
295    ///
296    /// # Returns
297    ///
298    /// `Ok(())` after the complete document is written.
299    ///
300    /// Runs a conservative payload preflight before encoding, then enforces
301    /// `limits` authoritatively while encoding the full envelope. The preflight
302    /// may report a lower-bound budget error and does not bound every
303    /// preparation allocation.
304    ///
305    /// The complete document is buffered and admitted before this method writes
306    /// its bytes to the destination. Preflight, serialization, or budget
307    /// rejection does not write this document. An I/O error may leave an
308    /// accepted prefix in the writer; do not blindly retry a complete
309    /// document on the same destination.
310    ///
311    /// This method does not explicitly flush the writer. Success means the
312    /// writer accepted all document bytes, not that they were flushed,
313    /// persisted, or received by a remote peer. Pass `&mut writer` to
314    /// retain ownership of a buffered writer, then call `flush()` and
315    /// handle its error separately. Passing an owned writer may invoke that
316    /// writer's drop behavior; errors from an implicit drop-time flush are
317    /// not returned by this method.
318    ///
319    /// # Errors
320    ///
321    /// Returns [`ValueWireEncodeError`] when preflight or final encoding
322    /// exceeds `limits`, the document cannot be serialized, or `writer`
323    /// rejects output.
324    #[cfg(feature = "json")]
325    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
326    pub fn to_json_writer_with_limits<W>(
327        &self,
328        mut writer: W,
329        limits: JsonEncodeLimits,
330    ) -> Result<(), ValueWireEncodeError>
331    where
332        W: Write,
333    {
334        ValueWireEncodePreflight::new(limits)
335            .check_container(self.container())
336            .map_err(ValueWireEncodeError::from)?;
337        let session = JsonEncodeSession::from_limits(limits);
338        JsonEncoder::new(session)
339            .write_buffered(&mut writer, self)
340            .map_err(ValueWireEncodeError::from)
341    }
342
343    /// Consumes the DTO and returns its runtime container.
344    ///
345    /// # Returns
346    ///
347    /// The preserved runtime container.
348    #[inline(always)]
349    pub fn into_container(self) -> ValueContainer {
350        self.value.into_container()
351    }
352}
353
354impl TryFrom<Value> for ValueWireV1 {
355    /// Failure to represent the consumed runtime value under the V1 contract.
356    type Error = ValueWireEncodeError;
357
358    /// Wraps a runtime scalar in a V1 DTO.
359    ///
360    /// # Parameters
361    ///
362    /// * `value` - Runtime scalar to validate and consume.
363    ///
364    /// # Returns
365    ///
366    /// A V1 DTO preserving the input type and shape.
367    ///
368    /// # Errors
369    ///
370    /// Returns [`ValueWireEncodeError::NonFiniteFloat`] for a non-finite
371    /// float, [`ValueWireEncodeError::BigDecimalScaleTooLarge`] for a decimal
372    /// whose exponent is outside V1's supported range, or
373    /// [`ValueWireEncodeError::InstantYearOutOfRange`] for an instant outside
374    /// the supported UTC year range.
375    /// With the `json` feature, returns `JsonNumberNotRoundTrippable` if a
376    /// runtime JSON number, including a nested number, cannot preserve its
377    /// `serde_json::Number` identity under the V1 number model.
378    #[inline(always)]
379    fn try_from(value: Value) -> Result<Self, Self::Error> {
380        ValueWirePayloadV1::try_from(value).map(Self::new)
381    }
382}
383
384impl TryFrom<MultiValues> for ValueWireV1 {
385    /// Failure to represent the consumed runtime value under the V1 contract.
386    type Error = ValueWireEncodeError;
387
388    /// Wraps a runtime collection in a V1 DTO.
389    ///
390    /// # Parameters
391    ///
392    /// * `values` - Homogeneous collection to validate and consume.
393    ///
394    /// # Returns
395    ///
396    /// A V1 DTO preserving the input type and shape.
397    ///
398    /// # Errors
399    ///
400    /// Returns [`ValueWireEncodeError::NonFiniteFloat`] for a non-finite
401    /// float element, [`ValueWireEncodeError::BigDecimalScaleTooLarge`] for
402    /// an element whose decimal exponent is outside V1's supported range, or
403    /// [`ValueWireEncodeError::InstantYearOutOfRange`] for an instant outside
404    /// the supported UTC year range.
405    /// With the `json` feature, returns `JsonNumberNotRoundTrippable` if any
406    /// runtime JSON number in the collection, including a nested number,
407    /// cannot preserve its `serde_json::Number` identity under the V1 number
408    /// model.
409    #[inline(always)]
410    fn try_from(values: MultiValues) -> Result<Self, Self::Error> {
411        ValueWirePayloadV1::try_from(values).map(Self::new)
412    }
413}
414
415impl TryFrom<ValueContainer> for ValueWireV1 {
416    /// Failure to represent the consumed runtime value under the V1 contract.
417    type Error = ValueWireEncodeError;
418
419    /// Wraps an explicit runtime shape in a V1 DTO.
420    ///
421    /// # Parameters
422    ///
423    /// * `value` - Scalar-or-collection container to validate and consume.
424    ///
425    /// # Returns
426    ///
427    /// A V1 DTO preserving the input type and shape.
428    ///
429    /// # Errors
430    ///
431    /// Returns [`ValueWireEncodeError::NonFiniteFloat`] for a non-finite
432    /// float, [`ValueWireEncodeError::BigDecimalScaleTooLarge`] for a decimal
433    /// whose exponent is outside V1's supported range, or
434    /// [`ValueWireEncodeError::InstantYearOutOfRange`] for an instant outside
435    /// the supported UTC year range.
436    /// With the `json` feature, returns `JsonNumberNotRoundTrippable` if a
437    /// runtime JSON number in either the scalar or collection shape, including
438    /// a nested number, cannot preserve its `serde_json::Number` identity
439    /// under the V1 number model.
440    #[inline]
441    fn try_from(value: ValueContainer) -> Result<Self, Self::Error> {
442        ValueWirePayloadV1::try_from(value).map(Self::new)
443    }
444}
445
446impl From<ValueWireV1> for ValueContainer {
447    /// Unwraps the runtime container from a V1 DTO.
448    ///
449    /// # Parameters
450    ///
451    /// * `value` - Owned DTO whose container is moved into the result.
452    ///
453    /// # Returns
454    ///
455    /// The preserved runtime container without cloning its payload.
456    #[inline(always)]
457    fn from(value: ValueWireV1) -> Self {
458        value.into_container()
459    }
460}
461
462impl Serialize for ValueWireV1 {
463    /// Serializes the contained runtime shape through the V1 contract.
464    ///
465    /// # Type Parameters
466    ///
467    /// * `S` - Serde serializer defining the output format and error type.
468    ///
469    /// # Parameters
470    ///
471    /// * `serializer` - Serializer consumed to emit the versioned envelope.
472    ///
473    /// # Returns
474    ///
475    /// The serializer's completed output on success.
476    ///
477    /// # Errors
478    ///
479    /// Returns the serializer's error if it cannot encode the envelope or
480    /// payload.
481    #[inline(always)]
482    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
483    where
484        S: Serializer,
485    {
486        serialize_wire(self.value.container().into(), serializer)
487    }
488}