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}