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(
147 input: &[u8],
148 limits: JsonDecodeLimits,
149 ) -> Result<Self, ValueWireDecodeError> {
150 let session = JsonDecodeSession::from_limits(limits);
151 JsonDecoder::new(session)
152 .decode_unique_seed_utf8(ValueWirePayloadV1Seed::new(), input)
153 .map_err(ValueWireDecodeError::from)
154 }
155
156 /// Wraps a payload decoded through V1's finite-number Serde adapters.
157 ///
158 /// # Parameters
159 ///
160 /// * `value` - Already validated decoded runtime container.
161 ///
162 /// # Returns
163 ///
164 /// An owned unversioned V1 payload.
165 #[inline]
166 pub(in crate::value_wire) const fn from_decoded(value: ValueContainer) -> Self {
167 Self { value }
168 }
169
170 /// Builds a payload after enforcing V1's representational constraints.
171 ///
172 /// # Parameters
173 ///
174 /// * `value` - Runtime container to validate and own.
175 ///
176 /// # Returns
177 ///
178 /// A validated owned V1 payload.
179 ///
180 /// # Errors
181 ///
182 /// Returns [`ValueWireEncodeError`] when any contained payload is not
183 /// representable by V1, including an instant outside the supported UTC
184 /// year range.
185 fn try_new(value: ValueContainer) -> Result<Self, ValueWireEncodeError> {
186 match &value {
187 ValueContainer::Scalar(value) => validate_value(value)?,
188 ValueContainer::Collection(values) => validate_values(values)?,
189 }
190 Ok(Self { value })
191 }
192
193 /// Borrows the preserved runtime value.
194 ///
195 /// # Returns
196 ///
197 /// A shared reference to the preserved scalar-or-collection container.
198 #[must_use = "the borrowed value container should be used"]
199 #[inline(always)]
200 pub const fn container(&self) -> &ValueContainer {
201 &self.value
202 }
203
204 /// Encodes this V1 payload into a compact JSON vector with default limits.
205 ///
206 /// # Returns
207 ///
208 /// Compact UTF-8 JSON bytes for this unversioned payload.
209 ///
210 /// # Errors
211 ///
212 /// Returns [`ValueWireEncodeError::Budget`] when the payload exceeds the
213 /// default JSON resource profile.
214 #[cfg(feature = "json")]
215 #[inline(always)]
216 #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
217 pub fn to_json_vec(&self) -> Result<Vec<u8>, ValueWireEncodeError> {
218 self.to_json_vec_with_limits(Self::default_json_encode_limits())
219 }
220
221 /// Encodes this V1 payload into a bounded compact JSON vector.
222 ///
223 /// # Parameters
224 ///
225 /// * `limits` - Resource limits enforced during encoding.
226 ///
227 /// # Returns
228 ///
229 /// Compact UTF-8 JSON bytes for this unversioned payload.
230 ///
231 /// Runs a conservative payload preflight before encoding, then enforces
232 /// `limits` authoritatively while encoding. The preflight may report a
233 /// lower-bound budget error and does not bound every preparation
234 /// allocation.
235 ///
236 /// # Errors
237 ///
238 /// Returns [`ValueWireEncodeError`] when preflight or final encoding
239 /// exceeds `limits`, or the payload cannot be serialized.
240 #[cfg(feature = "json")]
241 #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
242 pub fn to_json_vec_with_limits(
243 &self,
244 limits: JsonEncodeLimits,
245 ) -> Result<Vec<u8>, ValueWireEncodeError> {
246 ValueWireEncodePreflight::new(limits)
247 .check_container(self.container())
248 .map_err(ValueWireEncodeError::from)?;
249 let session = JsonEncodeSession::from_limits(limits);
250 JsonEncoder::new(session)
251 .to_vec(self)
252 .map_err(ValueWireEncodeError::from)
253 }
254
255 /// Encodes this V1 payload to a writer with default limits.
256 ///
257 /// # Type Parameters
258 ///
259 /// * `W` - Destination writer type.
260 ///
261 /// # Parameters
262 ///
263 /// * `writer` - Destination receiving the complete JSON payload.
264 ///
265 /// # Returns
266 ///
267 /// `Ok(())` after the complete payload is written.
268 ///
269 /// The complete payload is buffered and admitted before this method writes
270 /// its bytes to the destination. Preflight, serialization, or budget
271 /// rejection does not write this payload. An I/O error may leave an
272 /// accepted prefix in the writer; do not blindly retry a complete
273 /// payload on the same destination.
274 ///
275 /// This method does not explicitly flush the writer. Success means the
276 /// writer accepted all payload bytes, not that they were flushed,
277 /// persisted, or received by a remote peer. Pass `&mut writer` to
278 /// retain ownership of a buffered writer, then call `flush()` and
279 /// handle its error separately. Passing an owned writer may invoke that
280 /// writer's drop behavior; errors from an implicit drop-time flush are
281 /// not returned by this method.
282 ///
283 /// # Errors
284 ///
285 /// Returns [`ValueWireEncodeError::Budget`] for resource-limit failures or
286 /// [`ValueWireEncodeError::Io`] when `writer` rejects output.
287 #[cfg(feature = "json")]
288 #[inline(always)]
289 #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
290 pub fn to_json_writer<W>(&self, writer: W) -> Result<(), ValueWireEncodeError>
291 where
292 W: Write,
293 {
294 self.to_json_writer_with_limits(writer, Self::default_json_encode_limits())
295 }
296
297 /// Encodes this V1 payload to a writer after enforcing JSON budgets.
298 ///
299 /// # Type Parameters
300 ///
301 /// * `W` - Destination writer type.
302 ///
303 /// # Parameters
304 ///
305 /// * `writer` - Destination receiving the complete JSON payload.
306 /// * `limits` - Resource limits enforced during encoding.
307 ///
308 /// # Returns
309 ///
310 /// `Ok(())` after the complete payload is written.
311 ///
312 /// Runs a conservative payload preflight before encoding, then enforces
313 /// `limits` authoritatively while encoding. The preflight may report a
314 /// lower-bound budget error and does not bound every preparation
315 /// allocation.
316 ///
317 /// The complete payload is buffered and admitted before this method writes
318 /// its bytes to the destination. Preflight, serialization, or budget
319 /// rejection does not write this payload. An I/O error may leave an
320 /// accepted prefix in the writer; do not blindly retry a complete
321 /// payload on the same destination.
322 ///
323 /// This method does not explicitly flush the writer. Success means the
324 /// writer accepted all payload bytes, not that they were flushed,
325 /// persisted, or received by a remote peer. Pass `&mut writer` to
326 /// retain ownership of a buffered writer, then call `flush()` and
327 /// handle its error separately. Passing an owned writer may invoke that
328 /// writer's drop behavior; errors from an implicit drop-time flush are
329 /// not returned by this method.
330 ///
331 /// # Errors
332 ///
333 /// Returns [`ValueWireEncodeError`] when preflight or final encoding
334 /// exceeds `limits`, the payload cannot be serialized, or `writer` rejects
335 /// output.
336 #[cfg(feature = "json")]
337 #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
338 pub fn to_json_writer_with_limits<W>(
339 &self,
340 mut writer: W,
341 limits: JsonEncodeLimits,
342 ) -> Result<(), ValueWireEncodeError>
343 where
344 W: Write,
345 {
346 ValueWireEncodePreflight::new(limits)
347 .check_container(self.container())
348 .map_err(ValueWireEncodeError::from)?;
349 let session = JsonEncodeSession::from_limits(limits);
350 JsonEncoder::new(session)
351 .write_buffered(&mut writer, self)
352 .map_err(ValueWireEncodeError::from)
353 }
354
355 /// Consumes this payload and returns its runtime value.
356 ///
357 /// # Returns
358 ///
359 /// The preserved scalar-or-collection container.
360 #[inline(always)]
361 pub fn into_container(self) -> ValueContainer {
362 self.value
363 }
364}
365
366impl TryFrom<Value> for ValueWirePayloadV1 {
367 /// Validation failure when the runtime value cannot satisfy the V1 wire
368 /// contract.
369 type Error = ValueWireEncodeError;
370
371 /// Validates a scalar for use in a V1 payload.
372 ///
373 /// # Parameters
374 ///
375 /// * `value` - Scalar to validate and take ownership of.
376 ///
377 /// # Returns
378 ///
379 /// A validated owned unversioned V1 payload.
380 ///
381 /// # Errors
382 ///
383 /// Returns [`ValueWireEncodeError::NonFiniteFloat`] for a non-finite
384 /// float, [`ValueWireEncodeError::BigDecimalScaleTooLarge`] for a decimal
385 /// whose exponent is outside V1's supported range, or
386 /// [`ValueWireEncodeError::InstantYearOutOfRange`] for an instant outside
387 /// the supported UTC year range.
388 /// With the `json` feature, returns `JsonNumberNotRoundTrippable` if a
389 /// runtime JSON number, including a nested number, cannot preserve its
390 /// `serde_json::Number` identity under the V1 number model.
391 #[inline]
392 fn try_from(value: Value) -> Result<Self, Self::Error> {
393 Self::try_new(value.into())
394 }
395}
396
397impl TryFrom<MultiValues> for ValueWirePayloadV1 {
398 /// Validation failure when the runtime value cannot satisfy the V1 wire
399 /// contract.
400 type Error = ValueWireEncodeError;
401
402 /// Validates a collection for use in a V1 payload.
403 ///
404 /// # Parameters
405 ///
406 /// * `value` - Homogeneous collection to validate and take ownership of.
407 ///
408 /// # Returns
409 ///
410 /// A validated owned unversioned V1 payload.
411 ///
412 /// # Errors
413 ///
414 /// Returns [`ValueWireEncodeError::NonFiniteFloat`] for a non-finite
415 /// float element, [`ValueWireEncodeError::BigDecimalScaleTooLarge`] for
416 /// an element whose decimal exponent is outside V1's supported range, or
417 /// [`ValueWireEncodeError::InstantYearOutOfRange`] for an instant outside
418 /// the supported UTC year range.
419 /// With the `json` feature, returns `JsonNumberNotRoundTrippable` if any
420 /// runtime JSON number in the collection, including a nested number,
421 /// cannot preserve its `serde_json::Number` identity under the V1 number
422 /// model.
423 #[inline(always)]
424 fn try_from(value: MultiValues) -> Result<Self, Self::Error> {
425 Self::try_new(value.into())
426 }
427}
428
429impl TryFrom<ValueContainer> for ValueWirePayloadV1 {
430 /// Validation failure when the runtime value cannot satisfy the V1 wire
431 /// contract.
432 type Error = ValueWireEncodeError;
433
434 /// Validates an explicitly shaped value for use in a V1 payload.
435 ///
436 /// # Parameters
437 ///
438 /// * `value` - Scalar-or-collection container whose shape and ownership are
439 /// preserved.
440 ///
441 /// # Returns
442 ///
443 /// A validated owned unversioned V1 payload.
444 ///
445 /// # Errors
446 ///
447 /// Returns [`ValueWireEncodeError::NonFiniteFloat`] for a non-finite
448 /// float, [`ValueWireEncodeError::BigDecimalScaleTooLarge`] for a decimal
449 /// whose exponent is outside V1's supported range, or
450 /// [`ValueWireEncodeError::InstantYearOutOfRange`] for an instant outside
451 /// the supported UTC year range.
452 /// With the `json` feature, returns `JsonNumberNotRoundTrippable` if a
453 /// runtime JSON number in either the scalar or collection shape, including
454 /// a nested number, cannot preserve its `serde_json::Number` identity
455 /// under the V1 number model.
456 #[inline]
457 fn try_from(value: ValueContainer) -> Result<Self, Self::Error> {
458 Self::try_new(value)
459 }
460}
461
462impl From<ValueWirePayloadV1> for ValueContainer {
463 /// Extracts the shaped runtime value from a V1 payload without cloning.
464 ///
465 /// # Parameters
466 ///
467 /// * `value` - Owned payload to consume.
468 ///
469 /// # Returns
470 ///
471 /// The preserved runtime container with its original scalar-or-collection
472 /// shape.
473 #[inline]
474 fn from(value: ValueWirePayloadV1) -> Self {
475 value.into_container()
476 }
477}
478
479impl Serialize for ValueWirePayloadV1 {
480 /// Serializes the unversioned V1 shape through borrowed runtime values.
481 ///
482 /// # Type Parameters
483 ///
484 /// * `S` - Serde serializer selecting the output format and error type.
485 ///
486 /// # Parameters
487 ///
488 /// * `serializer` - Destination serializer receiving the
489 /// scalar-or-collection shape.
490 ///
491 /// # Returns
492 ///
493 /// The serializer's success value after emitting the complete payload.
494 ///
495 /// # Errors
496 ///
497 /// Returns the serializer's error if the destination rejects the shape or
498 /// values.
499 #[inline(always)]
500 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
501 where
502 S: Serializer,
503 {
504 WireShapeRef::from(&self.value).serialize(serializer)
505 }
506}