Skip to main content

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