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(&self, limits: JsonEncodeLimits) -> Result<Vec<u8>, ValueWireEncodeError> {
203        self.check_encode_preflight(limits)?;
204        let session = JsonEncodeSession::from_limits(limits);
205        JsonEncoder::new(session)
206            .to_vec(self)
207            .map_err(ValueWireEncodeError::from)
208    }
209
210    /// Encodes the borrowed V1 payload to a writer with default limits.
211    ///
212    /// # Type Parameters
213    ///
214    /// * `W` - Destination writer type.
215    ///
216    /// # Parameters
217    ///
218    /// * `writer` - Destination receiving the complete JSON payload.
219    ///
220    /// # Returns
221    ///
222    /// `Ok(())` after the complete payload is written.
223    ///
224    /// The complete payload is buffered and admitted before this method writes
225    /// its bytes to the destination. Preflight, serialization, or budget
226    /// rejection does not write this payload. An I/O error may leave an
227    /// accepted prefix in the writer; do not blindly retry a complete
228    /// payload on the same destination.
229    ///
230    /// This method does not explicitly flush the writer. Success means the
231    /// writer accepted all payload bytes, not that they were flushed,
232    /// persisted, or received by a remote peer. Pass `&mut writer` to
233    /// retain ownership of a buffered writer, then call `flush()` and
234    /// handle its error separately. Passing an owned writer may invoke that
235    /// writer's drop behavior; errors from an implicit drop-time flush are
236    /// not returned by this method.
237    ///
238    /// # Errors
239    ///
240    /// Returns [`ValueWireEncodeError`] for resource, serialization, or writer
241    /// failures.
242    #[cfg(feature = "json")]
243    #[inline(always)]
244    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
245    pub fn to_json_writer<W>(&self, writer: W) -> Result<(), ValueWireEncodeError>
246    where
247        W: Write,
248    {
249        self.to_json_writer_with_limits(writer, super::default_json_encode_limits())
250    }
251
252    /// Encodes the borrowed V1 payload to a writer with explicit limits.
253    ///
254    /// # Type Parameters
255    ///
256    /// * `W` - Destination writer type.
257    ///
258    /// # Parameters
259    ///
260    /// * `writer` - Destination receiving the complete JSON payload.
261    /// * `limits` - Resource limits enforced during JSON encoding.
262    ///
263    /// # Returns
264    ///
265    /// `Ok(())` after the complete payload is written.
266    ///
267    /// Runs a conservative payload preflight before encoding, then enforces
268    /// `limits` authoritatively while encoding. The preflight may report a
269    /// lower-bound budget error and does not bound every preparation
270    /// allocation.
271    ///
272    /// The complete payload is buffered and admitted before this method writes
273    /// its bytes to the destination. Preflight, serialization, or budget
274    /// rejection does not write this payload. An I/O error may leave an
275    /// accepted prefix in the writer; do not blindly retry a complete
276    /// payload on the same destination.
277    ///
278    /// This method does not explicitly flush the writer. Success means the
279    /// writer accepted all payload bytes, not that they were flushed,
280    /// persisted, or received by a remote peer. Pass `&mut writer` to
281    /// retain ownership of a buffered writer, then call `flush()` and
282    /// handle its error separately. Passing an owned writer may invoke that
283    /// writer's drop behavior; errors from an implicit drop-time flush are
284    /// not returned by this method.
285    ///
286    /// # Errors
287    ///
288    /// Returns [`ValueWireEncodeError`] when the payload preflight or final
289    /// encoding exceeds `limits`, Serde rejects the payload, or `writer`
290    /// rejects output.
291    #[cfg(feature = "json")]
292    #[inline]
293    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
294    pub fn to_json_writer_with_limits<W>(
295        &self,
296        mut writer: W,
297        limits: JsonEncodeLimits,
298    ) -> Result<(), ValueWireEncodeError>
299    where
300        W: Write,
301    {
302        self.check_encode_preflight(limits)?;
303        let session = JsonEncodeSession::from_limits(limits);
304        JsonEncoder::new(session)
305            .write_buffered(&mut writer, self)
306            .map_err(ValueWireEncodeError::from)
307    }
308
309    /// Returns the borrowed internal shape used by V1 serialization.
310    ///
311    /// # Returns
312    ///
313    /// A copyable borrowed view of the validated scalar-or-collection shape.
314    #[must_use]
315    #[inline(always)]
316    pub(in crate::value_wire) fn shape(&self) -> WireShapeRef<'a> {
317        match self.source {
318            WireSourceRef::Scalar(value) => WireShapeRef::Scalar(value.into()),
319            WireSourceRef::Collection(values) => WireShapeRef::Collection(values.into()),
320        }
321    }
322
323    /// Checks the borrowed runtime payload against conservative JSON limits.
324    ///
325    /// # Parameters
326    ///
327    /// * `limits` - JSON resource limits used to check payload preparation.
328    ///
329    /// # Returns
330    ///
331    /// `Ok(())` when the conservative payload preflight accepts the source.
332    ///
333    /// # Errors
334    ///
335    /// Returns [`ValueWireEncodeError`] when a lower-bound payload estimate
336    /// exceeds `limits`. The final encoder performs the authoritative check.
337    #[cfg(feature = "json")]
338    pub(in crate::value_wire) fn check_encode_preflight(
339        &self,
340        limits: JsonEncodeLimits,
341    ) -> Result<(), ValueWireEncodeError> {
342        let mut checker = ValueWireEncodePreflight::new(limits);
343        match self.source {
344            WireSourceRef::Scalar(value) => checker.check_value(value),
345            WireSourceRef::Collection(values) => checker.check_values(values),
346        }
347        .map_err(ValueWireEncodeError::from)
348    }
349}
350
351/// Validates one scalar against V1's JSON finite-float invariant.
352///
353/// # Parameters
354///
355/// * `value` - Scalar value to validate without modifying it.
356///
357/// # Returns
358///
359/// `Ok(())` when the scalar is representable by the V1 wire contract.
360///
361/// # Errors
362///
363/// Returns [`ValueWireEncodeError::NonFiniteFloat`] for a non-finite float,
364/// [`ValueWireEncodeError::BigDecimalScaleTooLarge`] for an unsupported
365/// arbitrary-precision decimal scale, or
366/// [`ValueWireEncodeError::InstantYearOutOfRange`] for an Instant whose UTC
367/// year is outside 0 through 9999.
368/// With the `json` feature, returns `JsonNumberNotRoundTrippable` if a runtime
369/// JSON number, including a nested number, cannot preserve its
370/// `serde_json::Number` identity under the V1 number model.
371pub(in crate::value_wire) fn validate_value(value: &Value) -> Result<(), ValueWireEncodeError> {
372    #[cfg(feature = "json")]
373    if let ValueRepr::Json(value) = &value.repr {
374        validate_numbers(value)?;
375    }
376    #[cfg(feature = "chrono")]
377    if let ValueRepr::Instant(value) = &value.repr {
378        validate_instant_year(value.year())?;
379    }
380    #[cfg(feature = "big-decimal")]
381    if let ValueRepr::BigDecimal(value) = &value.repr {
382        validate_big_decimal_scale(value.as_bigint_and_scale().1)?;
383    }
384    let non_finite = matches!(&value.repr, ValueRepr::Float32(value) if !value.is_finite())
385        || matches!(&value.repr, ValueRepr::Float64(value) if !value.is_finite());
386    if non_finite {
387        return Err(ValueWireEncodeError::NonFiniteFloat {
388            data_type: value.data_type(),
389        });
390    }
391    Ok(())
392}
393
394/// Validates one collection against the V1 wire representation constraints.
395///
396/// # Parameters
397///
398/// * `values` - Collection to validate without modifying it.
399///
400/// # Returns
401///
402/// `Ok(())` when every element is representable by the V1 wire contract.
403///
404/// # Errors
405///
406/// Returns [`ValueWireEncodeError::NonFiniteFloat`] for a non-finite float
407/// element, [`ValueWireEncodeError::BigDecimalScaleTooLarge`] for an
408/// unsupported arbitrary-precision decimal scale, or
409/// [`ValueWireEncodeError::InstantYearOutOfRange`] for an Instant whose UTC
410/// year is outside 0 through 9999.
411/// With the `json` feature, returns `JsonNumberNotRoundTrippable` if any
412/// runtime JSON number in the collection, including a nested number, cannot
413/// preserve its `serde_json::Number` identity under the V1 number model.
414pub(in crate::value_wire) fn validate_values(values: &MultiValues) -> Result<(), ValueWireEncodeError> {
415    #[cfg(feature = "json")]
416    if let MultiValuesRepr::Json(values) = &values.repr {
417        for value in values {
418            validate_numbers(value)?;
419        }
420    }
421    #[cfg(feature = "chrono")]
422    if let MultiValuesRepr::Instant(values) = &values.repr {
423        for value in values {
424            validate_instant_year(value.year())?;
425        }
426    }
427    #[cfg(feature = "big-decimal")]
428    if let MultiValuesRepr::BigDecimal(values) = &values.repr {
429        for value in values {
430            validate_big_decimal_scale(value.as_bigint_and_scale().1)?;
431        }
432    }
433    let non_finite = match &values.repr {
434        MultiValuesRepr::Float32(values) => values.iter().any(|value| !value.is_finite()),
435        MultiValuesRepr::Float64(values) => values.iter().any(|value| !value.is_finite()),
436        _ => false,
437    };
438    if non_finite {
439        return Err(ValueWireEncodeError::NonFiniteFloat {
440            data_type: values.data_type(),
441        });
442    }
443    Ok(())
444}
445
446/// Validates the decimal exponent accepted by V1's bounded payload format.
447///
448/// # Parameters
449///
450/// * `scale` - Decimal scale to compare with the V1 inclusive bound.
451///
452/// # Returns
453///
454/// `Ok(())` when the scale is within the supported magnitude.
455///
456/// # Errors
457///
458/// Returns [`ValueWireEncodeError::BigDecimalScaleTooLarge`] when `scale`
459/// exceeds the V1 bound.
460#[cfg(feature = "big-decimal")]
461#[inline]
462fn validate_big_decimal_scale(scale: i64) -> Result<(), ValueWireEncodeError> {
463    if is_valid_big_decimal_scale(scale) {
464        return Ok(());
465    }
466    Err(ValueWireEncodeError::BigDecimalScaleTooLarge {
467        scale,
468        maximum_absolute_scale: MAX_BIG_DECIMAL_ABSOLUTE_SCALE,
469    })
470}
471
472impl<'a> TryFrom<&'a Value> for ValueWirePayloadRefV1<'a> {
473    /// V1 representation validation failure; the borrowed input is unchanged.
474    type Error = ValueWireEncodeError;
475
476    /// Borrows and validates a scalar.
477    ///
478    /// # Parameters
479    ///
480    /// * `value` - Scalar runtime value to validate and borrow.
481    ///
482    /// # Returns
483    ///
484    /// A validated V1 payload borrowing the original input without cloning.
485    ///
486    /// # Errors
487    ///
488    /// Returns the representation validation errors documented by
489    /// [`Self::from_value`].
490    #[inline]
491    fn try_from(value: &'a Value) -> Result<Self, Self::Error> {
492        Self::from_value(value)
493    }
494}
495
496impl<'a> TryFrom<&'a MultiValues> for ValueWirePayloadRefV1<'a> {
497    /// V1 representation validation failure; the borrowed input is unchanged.
498    type Error = ValueWireEncodeError;
499
500    /// Borrows and validates a collection.
501    ///
502    /// # Parameters
503    ///
504    /// * `values` - Homogeneous runtime collection to validate and borrow.
505    ///
506    /// # Returns
507    ///
508    /// A validated V1 payload borrowing the original input without cloning.
509    ///
510    /// # Errors
511    ///
512    /// Returns the representation validation errors documented by
513    /// [`Self::from_values`].
514    #[inline]
515    fn try_from(values: &'a MultiValues) -> Result<Self, Self::Error> {
516        Self::from_values(values)
517    }
518}
519
520impl<'a> TryFrom<&'a ValueContainer> for ValueWirePayloadRefV1<'a> {
521    /// V1 representation validation failure; the borrowed input is unchanged.
522    type Error = ValueWireEncodeError;
523
524    /// Borrows and validates an explicit shape.
525    ///
526    /// # Parameters
527    ///
528    /// * `value` - Explicit scalar-or-collection value to validate and borrow.
529    ///
530    /// # Returns
531    ///
532    /// A validated V1 payload borrowing the original input without cloning.
533    ///
534    /// # Errors
535    ///
536    /// Returns the representation validation errors documented by
537    /// [`Self::from_container`].
538    #[inline]
539    fn try_from(value: &'a ValueContainer) -> Result<Self, Self::Error> {
540        Self::from_container(value)
541    }
542}
543
544impl Serialize for ValueWirePayloadRefV1<'_> {
545    /// Serializes the borrowed unversioned V1 shape without cloning its source.
546    ///
547    /// # Type Parameters
548    ///
549    /// * `S` - Serializer determining the output format and error type.
550    ///
551    /// # Parameters
552    ///
553    /// * `serializer` - Destination serializer consumed by this operation.
554    ///
555    /// # Returns
556    ///
557    /// The serializer's success value after emitting the scalar or collection.
558    ///
559    /// # Errors
560    ///
561    /// Returns `S::Error` if the serializer rejects the borrowed wire shape.
562    #[inline]
563    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
564    where
565        S: Serializer,
566    {
567        self.shape().serialize(serializer)
568    }
569}