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(input: &[u8], limits: JsonDecodeLimits) -> Result<Self, ValueWireDecodeError> {
147        let session = JsonDecodeSession::from_limits(limits);
148        JsonDecoder::new(session)
149            .decode_unique_seed_utf8(ValueWirePayloadV1Seed::new(), input)
150            .map_err(ValueWireDecodeError::from)
151    }
152
153    /// Wraps a payload decoded through V1's finite-number Serde adapters.
154    ///
155    /// # Parameters
156    ///
157    /// * `value` - Already validated decoded runtime container.
158    ///
159    /// # Returns
160    ///
161    /// An owned unversioned V1 payload.
162    #[inline]
163    pub(in crate::value_wire) const fn from_decoded(value: ValueContainer) -> Self {
164        Self { value }
165    }
166
167    /// Builds a payload after enforcing V1's representational constraints.
168    ///
169    /// # Parameters
170    ///
171    /// * `value` - Runtime container to validate and own.
172    ///
173    /// # Returns
174    ///
175    /// A validated owned V1 payload.
176    ///
177    /// # Errors
178    ///
179    /// Returns [`ValueWireEncodeError`] when any contained payload is not
180    /// representable by V1, including an instant outside the supported UTC
181    /// year range.
182    fn try_new(value: ValueContainer) -> Result<Self, ValueWireEncodeError> {
183        match &value {
184            ValueContainer::Scalar(value) => validate_value(value)?,
185            ValueContainer::Collection(values) => validate_values(values)?,
186        }
187        Ok(Self { value })
188    }
189
190    /// Borrows the preserved runtime value.
191    ///
192    /// # Returns
193    ///
194    /// A shared reference to the preserved scalar-or-collection container.
195    #[must_use = "the borrowed value container should be used"]
196    #[inline(always)]
197    pub const fn container(&self) -> &ValueContainer {
198        &self.value
199    }
200
201    /// Encodes this V1 payload into a compact JSON vector with default limits.
202    ///
203    /// # Returns
204    ///
205    /// Compact UTF-8 JSON bytes for this unversioned payload.
206    ///
207    /// # Errors
208    ///
209    /// Returns [`ValueWireEncodeError::Budget`] when the payload exceeds the
210    /// default JSON resource profile.
211    #[cfg(feature = "json")]
212    #[inline(always)]
213    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
214    pub fn to_json_vec(&self) -> Result<Vec<u8>, ValueWireEncodeError> {
215        self.to_json_vec_with_limits(Self::default_json_encode_limits())
216    }
217
218    /// Encodes this V1 payload into a bounded compact JSON vector.
219    ///
220    /// # Parameters
221    ///
222    /// * `limits` - Resource limits enforced during encoding.
223    ///
224    /// # Returns
225    ///
226    /// Compact UTF-8 JSON bytes for this unversioned payload.
227    ///
228    /// Runs a conservative payload preflight before encoding, then enforces
229    /// `limits` authoritatively while encoding. The preflight may report a
230    /// lower-bound budget error and does not bound every preparation
231    /// allocation.
232    ///
233    /// # Errors
234    ///
235    /// Returns [`ValueWireEncodeError`] when preflight or final encoding
236    /// exceeds `limits`, or the payload cannot be serialized.
237    #[cfg(feature = "json")]
238    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
239    pub fn to_json_vec_with_limits(&self, limits: JsonEncodeLimits) -> 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 payload 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 payload.
258    ///
259    /// # Returns
260    ///
261    /// `Ok(())` after the complete payload is written.
262    ///
263    /// The complete payload is buffered and admitted before this method writes
264    /// its bytes to the destination. Preflight, serialization, or budget
265    /// rejection does not write this payload. An I/O error may leave an
266    /// accepted prefix in the writer; do not blindly retry a complete
267    /// payload on the same destination.
268    ///
269    /// This method does not explicitly flush the writer. Success means the
270    /// writer accepted all payload 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 payload 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 payload.
300    /// * `limits` - Resource limits enforced during encoding.
301    ///
302    /// # Returns
303    ///
304    /// `Ok(())` after the complete payload is written.
305    ///
306    /// Runs a conservative payload preflight before encoding, then enforces
307    /// `limits` authoritatively while encoding. The preflight may report a
308    /// lower-bound budget error and does not bound every preparation
309    /// allocation.
310    ///
311    /// The complete payload is buffered and admitted before this method writes
312    /// its bytes to the destination. Preflight, serialization, or budget
313    /// rejection does not write this payload. An I/O error may leave an
314    /// accepted prefix in the writer; do not blindly retry a complete
315    /// payload on the same destination.
316    ///
317    /// This method does not explicitly flush the writer. Success means the
318    /// writer accepted all payload 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 payload cannot be serialized, or `writer` rejects
329    /// 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 this payload and returns its runtime value.
350    ///
351    /// # Returns
352    ///
353    /// The preserved scalar-or-collection container.
354    #[inline(always)]
355    pub fn into_container(self) -> ValueContainer {
356        self.value
357    }
358}
359
360impl TryFrom<Value> for ValueWirePayloadV1 {
361    /// Validation failure when the runtime value cannot satisfy the V1 wire
362    /// contract.
363    type Error = ValueWireEncodeError;
364
365    /// Validates a scalar for use in a V1 payload.
366    ///
367    /// # Parameters
368    ///
369    /// * `value` - Scalar to validate and take ownership of.
370    ///
371    /// # Returns
372    ///
373    /// A validated owned unversioned V1 payload.
374    ///
375    /// # Errors
376    ///
377    /// Returns [`ValueWireEncodeError::NonFiniteFloat`] for a non-finite
378    /// float, [`ValueWireEncodeError::BigDecimalScaleTooLarge`] for a decimal
379    /// whose exponent is outside V1's supported range, or
380    /// [`ValueWireEncodeError::InstantYearOutOfRange`] for an instant outside
381    /// the supported UTC year range.
382    /// With the `json` feature, returns `JsonNumberNotRoundTrippable` if a
383    /// runtime JSON number, including a nested number, cannot preserve its
384    /// `serde_json::Number` identity under the V1 number model.
385    #[inline]
386    fn try_from(value: Value) -> Result<Self, Self::Error> {
387        Self::try_new(value.into())
388    }
389}
390
391impl TryFrom<MultiValues> for ValueWirePayloadV1 {
392    /// Validation failure when the runtime value cannot satisfy the V1 wire
393    /// contract.
394    type Error = ValueWireEncodeError;
395
396    /// Validates a collection for use in a V1 payload.
397    ///
398    /// # Parameters
399    ///
400    /// * `value` - Homogeneous collection to validate and take ownership of.
401    ///
402    /// # Returns
403    ///
404    /// A validated owned unversioned V1 payload.
405    ///
406    /// # Errors
407    ///
408    /// Returns [`ValueWireEncodeError::NonFiniteFloat`] for a non-finite
409    /// float element, [`ValueWireEncodeError::BigDecimalScaleTooLarge`] for
410    /// an element whose decimal exponent is outside V1's supported range, or
411    /// [`ValueWireEncodeError::InstantYearOutOfRange`] for an instant outside
412    /// the supported UTC year range.
413    /// With the `json` feature, returns `JsonNumberNotRoundTrippable` if any
414    /// runtime JSON number in the collection, including a nested number,
415    /// cannot preserve its `serde_json::Number` identity under the V1 number
416    /// model.
417    #[inline(always)]
418    fn try_from(value: MultiValues) -> Result<Self, Self::Error> {
419        Self::try_new(value.into())
420    }
421}
422
423impl TryFrom<ValueContainer> for ValueWirePayloadV1 {
424    /// Validation failure when the runtime value cannot satisfy the V1 wire
425    /// contract.
426    type Error = ValueWireEncodeError;
427
428    /// Validates an explicitly shaped value for use in a V1 payload.
429    ///
430    /// # Parameters
431    ///
432    /// * `value` - Scalar-or-collection container whose shape and ownership are
433    ///   preserved.
434    ///
435    /// # Returns
436    ///
437    /// A validated owned unversioned V1 payload.
438    ///
439    /// # Errors
440    ///
441    /// Returns [`ValueWireEncodeError::NonFiniteFloat`] for a non-finite
442    /// float, [`ValueWireEncodeError::BigDecimalScaleTooLarge`] for a decimal
443    /// whose exponent is outside V1's supported range, or
444    /// [`ValueWireEncodeError::InstantYearOutOfRange`] for an instant outside
445    /// the supported UTC year range.
446    /// With the `json` feature, returns `JsonNumberNotRoundTrippable` if a
447    /// runtime JSON number in either the scalar or collection shape, including
448    /// a nested number, cannot preserve its `serde_json::Number` identity
449    /// under the V1 number model.
450    #[inline]
451    fn try_from(value: ValueContainer) -> Result<Self, Self::Error> {
452        Self::try_new(value)
453    }
454}
455
456impl From<ValueWirePayloadV1> for ValueContainer {
457    /// Extracts the shaped runtime value from a V1 payload without cloning.
458    ///
459    /// # Parameters
460    ///
461    /// * `value` - Owned payload to consume.
462    ///
463    /// # Returns
464    ///
465    /// The preserved runtime container with its original scalar-or-collection
466    /// shape.
467    #[inline]
468    fn from(value: ValueWirePayloadV1) -> Self {
469        value.into_container()
470    }
471}
472
473impl Serialize for ValueWirePayloadV1 {
474    /// Serializes the unversioned V1 shape through borrowed runtime values.
475    ///
476    /// # Type Parameters
477    ///
478    /// * `S` - Serde serializer selecting the output format and error type.
479    ///
480    /// # Parameters
481    ///
482    /// * `serializer` - Destination serializer receiving the
483    ///   scalar-or-collection shape.
484    ///
485    /// # Returns
486    ///
487    /// The serializer's success value after emitting the complete payload.
488    ///
489    /// # Errors
490    ///
491    /// Returns the serializer's error if the destination rejects the shape or
492    /// values.
493    #[inline(always)]
494    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
495    where
496        S: Serializer,
497    {
498        WireShapeRef::from(&self.value).serialize(serializer)
499    }
500}