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}