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}