Skip to main content

qubit_value/value_wire/
value_wire_payload_ref_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//! Borrowed V1 payload serialization.
10
11#[cfg(feature = "json")]
12use std::io::Write;
13
14#[cfg(feature = "chrono")]
15use chrono::Datelike;
16#[cfg(feature = "json")]
17use qubit_budget::json::JsonEncodeLimits;
18#[cfg(feature = "json")]
19use qubit_budget::json::JsonEncodeSession;
20#[cfg(feature = "json")]
21use qubit_json::encode::JsonEncoder;
22use serde::Serialize;
23use serde::Serializer;
24
25use super::ValueWireEncodeError;
26#[cfg(feature = "json")]
27use super::ValueWireEncodePreflight;
28use super::WireShapeRef;
29use super::internal::WireSourceRef;
30use crate::MultiValues;
31use crate::Value;
32use crate::ValueContainer;
33use crate::multi_values::MultiValuesRepr;
34use crate::value::ValueRepr;
35#[cfg(feature = "big-decimal")]
36use crate::wire::MAX_BIG_DECIMAL_ABSOLUTE_SCALE;
37#[cfg(feature = "big-decimal")]
38use crate::wire::is_valid_big_decimal_scale;
39#[cfg(feature = "json")]
40use crate::wire::json::validate_numbers;
41#[cfg(feature = "chrono")]
42use crate::wire::validate_instant_year;
43
44/// Borrowed unversioned V1 payload for serialization without cloning.
45///
46/// Runtime JSON numbers must round trip to the same `serde_json::Number` under
47/// the strict `qubit-json` number model. A number that would change identity
48/// returns the JSON-gated `JsonNumberNotRoundTrippable` error; the error does
49/// not expose the number text.
50///
51/// Use one of the fallible constructors or the corresponding `TryFrom` impl
52/// to create a payload. The private representation prevents callers from
53/// bypassing V1 validation by constructing an unchecked shape directly.
54///
55/// # Type Parameters
56///
57/// * `'a` - Lifetime of the runtime payload borrowed for serialization.
58///
59/// # Examples
60///
61/// ```
62/// use qubit_value::{Value, ValueWirePayloadRefV1};
63///
64/// let value = Value::from(42_i32);
65/// let payload = ValueWirePayloadRefV1::from_value(&value).unwrap();
66/// let encoded = serde_json::to_string(&payload).unwrap();
67/// assert_eq!(encoded, r#"{"scalar":{"int32":42}}"#);
68/// let owned = qubit_value::ValueWirePayloadV1::try_from(Value::from(42_i32)).unwrap();
69/// assert_eq!(encoded, serde_json::to_string(&owned).unwrap());
70/// ```
71#[must_use]
72pub struct ValueWirePayloadRefV1<'a> {
73    /// Borrowed source validated for V1 serialization.
74    source: WireSourceRef<'a>,
75}
76
77impl<'a> ValueWirePayloadRefV1<'a> {
78    /// Borrows a scalar after validating the V1 wire representation
79    /// constraints.
80    ///
81    /// # Parameters
82    ///
83    /// * `value` - Scalar runtime value to validate and borrow.
84    ///
85    /// # Returns
86    ///
87    /// A borrowed V1 payload preserving the scalar shape.
88    ///
89    /// # Errors
90    ///
91    /// Returns [`ValueWireEncodeError::NonFiniteFloat`] for a non-finite float,
92    /// [`ValueWireEncodeError::BigDecimalScaleTooLarge`] for an unsupported
93    /// decimal scale, or [`ValueWireEncodeError::InstantYearOutOfRange`] when
94    /// an Instant's UTC year is outside 0 through 9999.
95    /// With the `json` feature, returns `JsonNumberNotRoundTrippable` if a
96    /// runtime JSON number, including a nested number, cannot preserve its
97    /// `serde_json::Number` identity under the V1 number model.
98    pub fn from_value(value: &'a Value) -> Result<Self, ValueWireEncodeError> {
99        validate_value(value)?;
100        Ok(Self {
101            source: WireSourceRef::Scalar(value),
102        })
103    }
104
105    /// Borrows a collection after validating the V1 wire representation
106    /// constraints.
107    ///
108    /// # Parameters
109    ///
110    /// * `values` - Homogeneous runtime collection to validate and borrow.
111    ///
112    /// # Returns
113    ///
114    /// A borrowed V1 payload preserving the collection shape.
115    ///
116    /// # Errors
117    ///
118    /// Returns [`ValueWireEncodeError::NonFiniteFloat`] for a non-finite float
119    /// element, [`ValueWireEncodeError::BigDecimalScaleTooLarge`] for an
120    /// unsupported decimal scale, or
121    /// [`ValueWireEncodeError::InstantYearOutOfRange`] when an Instant's
122    /// UTC year is outside 0 through 9999.
123    /// With the `json` feature, returns `JsonNumberNotRoundTrippable` if any
124    /// runtime JSON number in the collection, including a nested number,
125    /// cannot preserve its `serde_json::Number` identity under the V1 number
126    /// model.
127    pub fn from_values(values: &'a MultiValues) -> Result<Self, ValueWireEncodeError> {
128        validate_values(values)?;
129        Ok(Self {
130            source: WireSourceRef::Collection(values),
131        })
132    }
133
134    /// Borrows an explicit shape after validating the V1 wire representation
135    /// constraints.
136    ///
137    /// # Parameters
138    ///
139    /// * `value` - Explicit scalar-or-collection value to validate and borrow.
140    ///
141    /// # Returns
142    ///
143    /// A borrowed V1 payload preserving the original shape.
144    ///
145    /// # Errors
146    ///
147    /// Returns [`ValueWireEncodeError::NonFiniteFloat`] for a non-finite float,
148    /// [`ValueWireEncodeError::BigDecimalScaleTooLarge`] for an unsupported
149    /// decimal scale, or [`ValueWireEncodeError::InstantYearOutOfRange`] when
150    /// an Instant's UTC year is outside 0 through 9999.
151    /// With the `json` feature, returns `JsonNumberNotRoundTrippable` if a
152    /// runtime JSON number in either the scalar or collection shape, including
153    /// a nested number, cannot preserve its `serde_json::Number` identity
154    /// under the V1 number model.
155    #[inline]
156    pub fn from_container(value: &'a ValueContainer) -> Result<Self, ValueWireEncodeError> {
157        match value {
158            ValueContainer::Scalar(value) => Self::from_value(value),
159            ValueContainer::Collection(values) => Self::from_values(values),
160        }
161    }
162
163    /// Encodes the borrowed V1 payload into a compact JSON vector.
164    ///
165    /// # Returns
166    ///
167    /// Compact UTF-8 JSON bytes for this unversioned payload.
168    ///
169    /// # Errors
170    ///
171    /// Returns [`ValueWireEncodeError`] when encoding exceeds the default
172    /// resource profile or Serde rejects the payload.
173    #[cfg(feature = "json")]
174    #[inline(always)]
175    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
176    pub fn to_json_vec(&self) -> Result<Vec<u8>, ValueWireEncodeError> {
177        self.to_json_vec_with_limits(super::default_json_encode_limits())
178    }
179
180    /// Encodes the borrowed V1 payload with explicit JSON resource limits.
181    ///
182    /// # Parameters
183    ///
184    /// * `limits` - Resource limits enforced during JSON encoding.
185    ///
186    /// # Returns
187    ///
188    /// Compact UTF-8 JSON bytes for this unversioned payload.
189    ///
190    /// Runs a conservative payload preflight before encoding, then enforces
191    /// `limits` authoritatively while encoding. The preflight may report a
192    /// lower-bound budget error and does not bound every preparation
193    /// allocation.
194    ///
195    /// # Errors
196    ///
197    /// Returns [`ValueWireEncodeError`] when the payload preflight or final
198    /// encoding exceeds `limits`, or Serde rejects the payload.
199    #[cfg(feature = "json")]
200    #[inline]
201    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
202    pub fn to_json_vec_with_limits(
203        &self,
204        limits: JsonEncodeLimits,
205    ) -> Result<Vec<u8>, ValueWireEncodeError> {
206        self.check_encode_preflight(limits)?;
207        let session = JsonEncodeSession::from_limits(limits);
208        JsonEncoder::new(session)
209            .to_vec(self)
210            .map_err(ValueWireEncodeError::from)
211    }
212
213    /// Encodes the borrowed V1 payload to a writer with default limits.
214    ///
215    /// # Type Parameters
216    ///
217    /// * `W` - Destination writer type.
218    ///
219    /// # Parameters
220    ///
221    /// * `writer` - Destination receiving the complete JSON payload.
222    ///
223    /// # Returns
224    ///
225    /// `Ok(())` after the complete payload is written.
226    ///
227    /// The complete payload is buffered and admitted before this method writes
228    /// its bytes to the destination. Preflight, serialization, or budget
229    /// rejection does not write this payload. An I/O error may leave an
230    /// accepted prefix in the writer; do not blindly retry a complete
231    /// payload on the same destination.
232    ///
233    /// This method does not explicitly flush the writer. Success means the
234    /// writer accepted all payload bytes, not that they were flushed,
235    /// persisted, or received by a remote peer. Pass `&mut writer` to
236    /// retain ownership of a buffered writer, then call `flush()` and
237    /// handle its error separately. Passing an owned writer may invoke that
238    /// writer's drop behavior; errors from an implicit drop-time flush are
239    /// not returned by this method.
240    ///
241    /// # Errors
242    ///
243    /// Returns [`ValueWireEncodeError`] for resource, serialization, or writer
244    /// failures.
245    #[cfg(feature = "json")]
246    #[inline(always)]
247    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
248    pub fn to_json_writer<W>(&self, writer: W) -> Result<(), ValueWireEncodeError>
249    where
250        W: Write,
251    {
252        self.to_json_writer_with_limits(writer, super::default_json_encode_limits())
253    }
254
255    /// Encodes the borrowed V1 payload to a writer with explicit limits.
256    ///
257    /// # Type Parameters
258    ///
259    /// * `W` - Destination writer type.
260    ///
261    /// # Parameters
262    ///
263    /// * `writer` - Destination receiving the complete JSON payload.
264    /// * `limits` - Resource limits enforced during JSON encoding.
265    ///
266    /// # Returns
267    ///
268    /// `Ok(())` after the complete payload is written.
269    ///
270    /// Runs a conservative payload preflight before encoding, then enforces
271    /// `limits` authoritatively while encoding. The preflight may report a
272    /// lower-bound budget error and does not bound every preparation
273    /// allocation.
274    ///
275    /// The complete payload is buffered and admitted before this method writes
276    /// its bytes to the destination. Preflight, serialization, or budget
277    /// rejection does not write this payload. An I/O error may leave an
278    /// accepted prefix in the writer; do not blindly retry a complete
279    /// payload on the same destination.
280    ///
281    /// This method does not explicitly flush the writer. Success means the
282    /// writer accepted all payload bytes, not that they were flushed,
283    /// persisted, or received by a remote peer. Pass `&mut writer` to
284    /// retain ownership of a buffered writer, then call `flush()` and
285    /// handle its error separately. Passing an owned writer may invoke that
286    /// writer's drop behavior; errors from an implicit drop-time flush are
287    /// not returned by this method.
288    ///
289    /// # Errors
290    ///
291    /// Returns [`ValueWireEncodeError`] when the payload preflight or final
292    /// encoding exceeds `limits`, Serde rejects the payload, or `writer`
293    /// rejects output.
294    #[cfg(feature = "json")]
295    #[inline]
296    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
297    pub fn to_json_writer_with_limits<W>(
298        &self,
299        mut writer: W,
300        limits: JsonEncodeLimits,
301    ) -> Result<(), ValueWireEncodeError>
302    where
303        W: Write,
304    {
305        self.check_encode_preflight(limits)?;
306        let session = JsonEncodeSession::from_limits(limits);
307        JsonEncoder::new(session)
308            .write_buffered(&mut writer, self)
309            .map_err(ValueWireEncodeError::from)
310    }
311
312    /// Returns the borrowed internal shape used by V1 serialization.
313    ///
314    /// # Returns
315    ///
316    /// A copyable borrowed view of the validated scalar-or-collection shape.
317    #[must_use]
318    #[inline(always)]
319    pub(in crate::value_wire) fn shape(&self) -> WireShapeRef<'a> {
320        match self.source {
321            WireSourceRef::Scalar(value) => WireShapeRef::Scalar(value.into()),
322            WireSourceRef::Collection(values) => WireShapeRef::Collection(values.into()),
323        }
324    }
325
326    /// Checks the borrowed runtime payload against conservative JSON limits.
327    ///
328    /// # Parameters
329    ///
330    /// * `limits` - JSON resource limits used to check payload preparation.
331    ///
332    /// # Returns
333    ///
334    /// `Ok(())` when the conservative payload preflight accepts the source.
335    ///
336    /// # Errors
337    ///
338    /// Returns [`ValueWireEncodeError`] when a lower-bound payload estimate
339    /// exceeds `limits`. The final encoder performs the authoritative check.
340    #[cfg(feature = "json")]
341    pub(in crate::value_wire) fn check_encode_preflight(
342        &self,
343        limits: JsonEncodeLimits,
344    ) -> Result<(), ValueWireEncodeError> {
345        let mut checker = ValueWireEncodePreflight::new(limits);
346        match self.source {
347            WireSourceRef::Scalar(value) => checker.check_value(value),
348            WireSourceRef::Collection(values) => checker.check_values(values),
349        }
350        .map_err(ValueWireEncodeError::from)
351    }
352}
353
354/// Validates one scalar against V1's JSON finite-float invariant.
355///
356/// # Parameters
357///
358/// * `value` - Scalar value to validate without modifying it.
359///
360/// # Returns
361///
362/// `Ok(())` when the scalar is representable by the V1 wire contract.
363///
364/// # Errors
365///
366/// Returns [`ValueWireEncodeError::NonFiniteFloat`] for a non-finite float,
367/// [`ValueWireEncodeError::BigDecimalScaleTooLarge`] for an unsupported
368/// arbitrary-precision decimal scale, or
369/// [`ValueWireEncodeError::InstantYearOutOfRange`] for an Instant whose UTC
370/// year is outside 0 through 9999.
371/// With the `json` feature, returns `JsonNumberNotRoundTrippable` if a runtime
372/// JSON number, including a nested number, cannot preserve its
373/// `serde_json::Number` identity under the V1 number model.
374pub(in crate::value_wire) fn validate_value(value: &Value) -> Result<(), ValueWireEncodeError> {
375    #[cfg(feature = "json")]
376    if let ValueRepr::Json(value) = &value.repr {
377        validate_numbers(value)?;
378    }
379    #[cfg(feature = "chrono")]
380    if let ValueRepr::Instant(value) = &value.repr {
381        validate_instant_year(value.year())?;
382    }
383    #[cfg(feature = "big-decimal")]
384    if let ValueRepr::BigDecimal(value) = &value.repr {
385        validate_big_decimal_scale(value.as_bigint_and_scale().1)?;
386    }
387    let non_finite = matches!(&value.repr, ValueRepr::Float32(value) if !value.is_finite())
388        || matches!(&value.repr, ValueRepr::Float64(value) if !value.is_finite());
389    if non_finite {
390        return Err(ValueWireEncodeError::NonFiniteFloat {
391            data_type: value.data_type(),
392        });
393    }
394    Ok(())
395}
396
397/// Validates one collection against the V1 wire representation constraints.
398///
399/// # Parameters
400///
401/// * `values` - Collection to validate without modifying it.
402///
403/// # Returns
404///
405/// `Ok(())` when every element is representable by the V1 wire contract.
406///
407/// # Errors
408///
409/// Returns [`ValueWireEncodeError::NonFiniteFloat`] for a non-finite float
410/// element, [`ValueWireEncodeError::BigDecimalScaleTooLarge`] for an
411/// unsupported arbitrary-precision decimal scale, or
412/// [`ValueWireEncodeError::InstantYearOutOfRange`] for an Instant whose UTC
413/// year is outside 0 through 9999.
414/// With the `json` feature, returns `JsonNumberNotRoundTrippable` if any
415/// runtime JSON number in the collection, including a nested number, cannot
416/// preserve its `serde_json::Number` identity under the V1 number model.
417pub(in crate::value_wire) fn validate_values(
418    values: &MultiValues,
419) -> Result<(), ValueWireEncodeError> {
420    #[cfg(feature = "json")]
421    if let MultiValuesRepr::Json(values) = &values.repr {
422        for value in values {
423            validate_numbers(value)?;
424        }
425    }
426    #[cfg(feature = "chrono")]
427    if let MultiValuesRepr::Instant(values) = &values.repr {
428        for value in values {
429            validate_instant_year(value.year())?;
430        }
431    }
432    #[cfg(feature = "big-decimal")]
433    if let MultiValuesRepr::BigDecimal(values) = &values.repr {
434        for value in values {
435            validate_big_decimal_scale(value.as_bigint_and_scale().1)?;
436        }
437    }
438    let non_finite = match &values.repr {
439        MultiValuesRepr::Float32(values) => values.iter().any(|value| !value.is_finite()),
440        MultiValuesRepr::Float64(values) => values.iter().any(|value| !value.is_finite()),
441        _ => false,
442    };
443    if non_finite {
444        return Err(ValueWireEncodeError::NonFiniteFloat {
445            data_type: values.data_type(),
446        });
447    }
448    Ok(())
449}
450
451/// Validates the decimal exponent accepted by V1's bounded payload format.
452///
453/// # Parameters
454///
455/// * `scale` - Decimal scale to compare with the V1 inclusive bound.
456///
457/// # Returns
458///
459/// `Ok(())` when the scale is within the supported magnitude.
460///
461/// # Errors
462///
463/// Returns [`ValueWireEncodeError::BigDecimalScaleTooLarge`] when `scale`
464/// exceeds the V1 bound.
465#[cfg(feature = "big-decimal")]
466#[inline]
467fn validate_big_decimal_scale(scale: i64) -> Result<(), ValueWireEncodeError> {
468    if is_valid_big_decimal_scale(scale) {
469        return Ok(());
470    }
471    Err(ValueWireEncodeError::BigDecimalScaleTooLarge {
472        scale,
473        maximum_absolute_scale: MAX_BIG_DECIMAL_ABSOLUTE_SCALE,
474    })
475}
476
477impl<'a> TryFrom<&'a Value> for ValueWirePayloadRefV1<'a> {
478    /// V1 representation validation failure; the borrowed input is unchanged.
479    type Error = ValueWireEncodeError;
480
481    /// Borrows and validates a scalar.
482    ///
483    /// # Parameters
484    ///
485    /// * `value` - Scalar runtime value to validate and borrow.
486    ///
487    /// # Returns
488    ///
489    /// A validated V1 payload borrowing the original input without cloning.
490    ///
491    /// # Errors
492    ///
493    /// Returns the representation validation errors documented by
494    /// [`Self::from_value`].
495    #[inline]
496    fn try_from(value: &'a Value) -> Result<Self, Self::Error> {
497        Self::from_value(value)
498    }
499}
500
501impl<'a> TryFrom<&'a MultiValues> for ValueWirePayloadRefV1<'a> {
502    /// V1 representation validation failure; the borrowed input is unchanged.
503    type Error = ValueWireEncodeError;
504
505    /// Borrows and validates a collection.
506    ///
507    /// # Parameters
508    ///
509    /// * `values` - Homogeneous runtime collection to validate and borrow.
510    ///
511    /// # Returns
512    ///
513    /// A validated V1 payload borrowing the original input without cloning.
514    ///
515    /// # Errors
516    ///
517    /// Returns the representation validation errors documented by
518    /// [`Self::from_values`].
519    #[inline]
520    fn try_from(values: &'a MultiValues) -> Result<Self, Self::Error> {
521        Self::from_values(values)
522    }
523}
524
525impl<'a> TryFrom<&'a ValueContainer> for ValueWirePayloadRefV1<'a> {
526    /// V1 representation validation failure; the borrowed input is unchanged.
527    type Error = ValueWireEncodeError;
528
529    /// Borrows and validates an explicit shape.
530    ///
531    /// # Parameters
532    ///
533    /// * `value` - Explicit scalar-or-collection value to validate and borrow.
534    ///
535    /// # Returns
536    ///
537    /// A validated V1 payload borrowing the original input without cloning.
538    ///
539    /// # Errors
540    ///
541    /// Returns the representation validation errors documented by
542    /// [`Self::from_container`].
543    #[inline]
544    fn try_from(value: &'a ValueContainer) -> Result<Self, Self::Error> {
545        Self::from_container(value)
546    }
547}
548
549impl Serialize for ValueWirePayloadRefV1<'_> {
550    /// Serializes the borrowed unversioned V1 shape without cloning its source.
551    ///
552    /// # Type Parameters
553    ///
554    /// * `S` - Serializer determining the output format and error type.
555    ///
556    /// # Parameters
557    ///
558    /// * `serializer` - Destination serializer consumed by this operation.
559    ///
560    /// # Returns
561    ///
562    /// The serializer's success value after emitting the scalar or collection.
563    ///
564    /// # Errors
565    ///
566    /// Returns `S::Error` if the serializer rejects the borrowed wire shape.
567    #[inline]
568    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
569    where
570        S: Serializer,
571    {
572        self.shape().serialize(serializer)
573    }
574}