qubit_value/value_wire/value_wire_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//! Public DTO for the stable version-one JSON wire contract.
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::encode::JsonEncoder;
24use serde::Serialize;
25use serde::Serializer;
26
27use super::VALUE_WIRE_V1_VERSION;
28#[cfg(feature = "json")]
29use super::ValueWireDecodeError;
30use super::ValueWireEncodeError;
31#[cfg(feature = "json")]
32use super::ValueWireEncodePreflight;
33use super::ValueWirePayloadV1;
34use super::serialize_wire;
35use crate::MultiValues;
36use crate::Value;
37use crate::ValueContainer;
38
39/// Stable version-one wire DTO for a scalar or homogeneous collection.
40///
41/// Canonical JSON output is byte-stable for the same value under the
42/// supported `serde_json` version and configuration, with canonical field
43/// and object-key order, including recursively nested JSON objects. Other
44/// Serde formats may represent this DTO, but their bytes are outside this
45/// stability contract. See the [crate documentation](crate).
46///
47/// V1 is closed: existing tags, shapes, and payload representations cannot
48/// change, and future runtime data types require a new wire version instead
49/// of extending V1.
50///
51/// Runtime JSON numbers are accepted only when V1 can reconstruct the same
52/// `serde_json::Number` under the strict `qubit-json` number model. Numbers
53/// that would change identity when round-tripped return the JSON-gated
54/// `JsonNumberNotRoundTrippable` error. This error does not expose the number
55/// text.
56///
57/// Deserialization is intentionally available through
58/// [`crate::ValueWireV1Seed`], which lets a bounded decoder control the
59/// complete input and structure.
60///
61/// # Examples
62///
63/// ```
64/// use qubit_value::{Value, ValueWireV1};
65///
66/// let wire = ValueWireV1::try_from(Value::from(42_i32)).unwrap();
67/// let encoded = serde_json::to_string(&wire).unwrap();
68/// assert_eq!(encoded, r#"{"version":1,"value":{"scalar":{"int32":42}}}"#);
69/// assert_eq!(wire.into_container().get_first::<i32>().unwrap(), 42);
70/// ```
71#[must_use]
72#[derive(Debug, Clone, PartialEq, Eq, Hash)]
73pub struct ValueWireV1 {
74 /// Explicit runtime shape and typed payload represented by this DTO.
75 value: ValueWirePayloadV1,
76}
77
78impl ValueWireV1 {
79 /// Numeric version emitted and accepted by this DTO.
80 pub const VERSION: u8 = VALUE_WIRE_V1_VERSION;
81
82 /// Creates a V1 DTO from an already validated V1 payload.
83 ///
84 /// # Parameters
85 ///
86 /// * `value` - Validated [`ValueWirePayloadV1`] moved into the envelope,
87 /// preserving its exact runtime type and scalar-or-collection shape.
88 ///
89 /// # Returns
90 ///
91 /// A V1 DTO containing `value`.
92 #[inline(always)]
93 pub const fn new(value: ValueWirePayloadV1) -> Self {
94 Self { value }
95 }
96
97 /// Decodes a V1 JSON wire value using the default structural limits.
98 ///
99 /// The complete input length and decoded structure are checked before the
100 /// value is returned. Embedded protocols should share one
101 /// [`qubit_budget::json::JsonDecodeSession`] across their complete
102 /// document.
103 ///
104 /// # Parameters
105 ///
106 /// * `input` - Complete UTF-8 JSON document to decode.
107 ///
108 /// # Returns
109 ///
110 /// The decoded V1 wire DTO.
111 ///
112 /// # Errors
113 ///
114 /// Returns a limit error when the input or decoded structure is too large,
115 /// [`ValueWireDecodeError::UnsupportedVersion`] when the envelope declares
116 /// another supported-width version, or
117 /// [`ValueWireDecodeError::Syntax`] when the input is not one valid JSON
118 /// document, or [`ValueWireDecodeError::InvalidJson`] when valid JSON
119 /// cannot be decoded as a V1 wire value.
120 #[cfg(feature = "json")]
121 #[inline]
122 #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
123 pub fn decode_json_slice(input: &[u8]) -> Result<Self, ValueWireDecodeError> {
124 Self::decode_json_slice_with_limits(input, Self::default_json_decode_limits())
125 }
126
127 /// Decodes a V1 JSON wire value using explicit structural limits.
128 ///
129 /// The complete input length and decoded structure are checked before the
130 /// value is returned. Embedded values should be checked through the outer
131 /// protocol's shared [`qubit_budget::json::JsonDecodeSession`].
132 ///
133 /// # Parameters
134 ///
135 /// * `input` - Complete UTF-8 JSON document to decode.
136 /// * `limits` - Shared encoded-input and structural limits.
137 ///
138 /// # Returns
139 ///
140 /// The decoded V1 wire DTO.
141 ///
142 /// # Errors
143 ///
144 /// Returns a limit error when `input` or its decoded structure exceeds
145 /// `limits`, [`ValueWireDecodeError::UnsupportedVersion`] when the envelope
146 /// declares another supported-width version,
147 /// [`ValueWireDecodeError::Syntax`] when the input is not one valid JSON
148 /// document, or [`ValueWireDecodeError::InvalidJson`] when valid JSON
149 /// cannot be decoded as a V1 wire value.
150 #[cfg(feature = "json")]
151 #[inline]
152 #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
153 pub fn decode_json_slice_with_limits(
154 input: &[u8],
155 limits: JsonDecodeLimits,
156 ) -> Result<Self, ValueWireDecodeError> {
157 let session = JsonDecodeSession::from_limits(limits);
158 super::decode_wire_json_slice_with_session(input, session)
159 }
160
161 /// Returns the default JSON resource profile for complete V1 documents.
162 ///
163 /// # Returns
164 ///
165 /// Decode limits suitable for one standalone V1 envelope.
166 #[cfg(feature = "json")]
167 #[must_use = "the V1 JSON profile should be applied to a budget"]
168 #[inline]
169 #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
170 pub fn default_json_decode_limits() -> JsonDecodeLimits {
171 super::default_json_decode_limits()
172 }
173
174 /// Returns the default JSON resource profile for encoding V1 documents.
175 ///
176 /// # Returns
177 ///
178 /// Encode limits suitable for one standalone V1 envelope.
179 #[cfg(feature = "json")]
180 #[must_use = "the V1 JSON profile should be applied to an encode session"]
181 #[inline(always)]
182 #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
183 pub fn default_json_encode_limits() -> JsonEncodeLimits {
184 super::default_json_encode_limits()
185 }
186
187 /// Returns the runtime container represented by this DTO.
188 ///
189 /// # Returns
190 ///
191 /// A shared reference to the preserved runtime container.
192 #[must_use = "the borrowed value container should be used"]
193 #[inline]
194 pub const fn container(&self) -> &ValueContainer {
195 self.value.container()
196 }
197
198 /// Encodes this V1 document into a compact JSON vector with default limits.
199 ///
200 /// # Returns
201 ///
202 /// The encoded complete V1 document.
203 ///
204 /// # Errors
205 ///
206 /// Returns [`ValueWireEncodeError::Budget`] when the document exceeds the
207 /// default JSON resource profile.
208 #[cfg(feature = "json")]
209 #[inline]
210 #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
211 pub fn to_json_vec(&self) -> Result<Vec<u8>, ValueWireEncodeError> {
212 self.to_json_vec_with_limits(Self::default_json_encode_limits())
213 }
214
215 /// Encodes this V1 document into a bounded compact JSON vector.
216 ///
217 /// # Parameters
218 ///
219 /// * `limits` - Resource limits enforced during encoding.
220 ///
221 /// # Returns
222 ///
223 /// Compact UTF-8 JSON bytes for the complete V1 document.
224 ///
225 /// Runs a conservative payload preflight before encoding, then enforces
226 /// `limits` authoritatively while encoding the full envelope. The preflight
227 /// may report a lower-bound budget error and does not bound every
228 /// preparation allocation.
229 ///
230 /// # Errors
231 ///
232 /// Returns [`ValueWireEncodeError`] when preflight or final encoding
233 /// exceeds `limits`, or the document cannot be serialized.
234 #[cfg(feature = "json")]
235 #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
236 pub fn to_json_vec_with_limits(
237 &self,
238 limits: JsonEncodeLimits,
239 ) -> 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 document 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 document.
258 ///
259 /// # Returns
260 ///
261 /// `Ok(())` after the complete document is written.
262 ///
263 /// The complete document is buffered and admitted before this method writes
264 /// its bytes to the destination. Preflight, serialization, or budget
265 /// rejection does not write this document. An I/O error may leave an
266 /// accepted prefix in the writer; do not blindly retry a complete
267 /// document on the same destination.
268 ///
269 /// This method does not explicitly flush the writer. Success means the
270 /// writer accepted all document 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 document 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 document.
300 /// * `limits` - Resource limits enforced during encoding.
301 ///
302 /// # Returns
303 ///
304 /// `Ok(())` after the complete document is written.
305 ///
306 /// Runs a conservative payload preflight before encoding, then enforces
307 /// `limits` authoritatively while encoding the full envelope. The preflight
308 /// may report a lower-bound budget error and does not bound every
309 /// preparation allocation.
310 ///
311 /// The complete document is buffered and admitted before this method writes
312 /// its bytes to the destination. Preflight, serialization, or budget
313 /// rejection does not write this document. An I/O error may leave an
314 /// accepted prefix in the writer; do not blindly retry a complete
315 /// document on the same destination.
316 ///
317 /// This method does not explicitly flush the writer. Success means the
318 /// writer accepted all document 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 document cannot be serialized, or `writer`
329 /// rejects 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 the DTO and returns its runtime container.
350 ///
351 /// # Returns
352 ///
353 /// The preserved runtime container.
354 #[inline(always)]
355 pub fn into_container(self) -> ValueContainer {
356 self.value.into_container()
357 }
358}
359
360impl TryFrom<Value> for ValueWireV1 {
361 /// Failure to represent the consumed runtime value under the V1 contract.
362 type Error = ValueWireEncodeError;
363
364 /// Wraps a runtime scalar in a V1 DTO.
365 ///
366 /// # Parameters
367 ///
368 /// * `value` - Runtime scalar to validate and consume.
369 ///
370 /// # Returns
371 ///
372 /// A V1 DTO preserving the input type and shape.
373 ///
374 /// # Errors
375 ///
376 /// Returns [`ValueWireEncodeError::NonFiniteFloat`] for a non-finite
377 /// float, [`ValueWireEncodeError::BigDecimalScaleTooLarge`] for a decimal
378 /// whose exponent is outside V1's supported range, or
379 /// [`ValueWireEncodeError::InstantYearOutOfRange`] for an instant outside
380 /// the supported UTC year range.
381 /// With the `json` feature, returns `JsonNumberNotRoundTrippable` if a
382 /// runtime JSON number, including a nested number, cannot preserve its
383 /// `serde_json::Number` identity under the V1 number model.
384 #[inline(always)]
385 fn try_from(value: Value) -> Result<Self, Self::Error> {
386 ValueWirePayloadV1::try_from(value).map(Self::new)
387 }
388}
389
390impl TryFrom<MultiValues> for ValueWireV1 {
391 /// Failure to represent the consumed runtime value under the V1 contract.
392 type Error = ValueWireEncodeError;
393
394 /// Wraps a runtime collection in a V1 DTO.
395 ///
396 /// # Parameters
397 ///
398 /// * `values` - Homogeneous collection to validate and consume.
399 ///
400 /// # Returns
401 ///
402 /// A V1 DTO preserving the input type and shape.
403 ///
404 /// # Errors
405 ///
406 /// Returns [`ValueWireEncodeError::NonFiniteFloat`] for a non-finite
407 /// float element, [`ValueWireEncodeError::BigDecimalScaleTooLarge`] for
408 /// an element whose decimal exponent is outside V1's supported range, or
409 /// [`ValueWireEncodeError::InstantYearOutOfRange`] for an instant outside
410 /// the supported UTC year range.
411 /// With the `json` feature, returns `JsonNumberNotRoundTrippable` if any
412 /// runtime JSON number in the collection, including a nested number,
413 /// cannot preserve its `serde_json::Number` identity under the V1 number
414 /// model.
415 #[inline(always)]
416 fn try_from(values: MultiValues) -> Result<Self, Self::Error> {
417 ValueWirePayloadV1::try_from(values).map(Self::new)
418 }
419}
420
421impl TryFrom<ValueContainer> for ValueWireV1 {
422 /// Failure to represent the consumed runtime value under the V1 contract.
423 type Error = ValueWireEncodeError;
424
425 /// Wraps an explicit runtime shape in a V1 DTO.
426 ///
427 /// # Parameters
428 ///
429 /// * `value` - Scalar-or-collection container to validate and consume.
430 ///
431 /// # Returns
432 ///
433 /// A V1 DTO preserving the input type and shape.
434 ///
435 /// # Errors
436 ///
437 /// Returns [`ValueWireEncodeError::NonFiniteFloat`] for a non-finite
438 /// float, [`ValueWireEncodeError::BigDecimalScaleTooLarge`] for a decimal
439 /// whose exponent is outside V1's supported range, or
440 /// [`ValueWireEncodeError::InstantYearOutOfRange`] for an instant outside
441 /// the supported UTC year range.
442 /// With the `json` feature, returns `JsonNumberNotRoundTrippable` if a
443 /// runtime JSON number in either the scalar or collection shape, including
444 /// a nested number, cannot preserve its `serde_json::Number` identity
445 /// under the V1 number model.
446 #[inline]
447 fn try_from(value: ValueContainer) -> Result<Self, Self::Error> {
448 ValueWirePayloadV1::try_from(value).map(Self::new)
449 }
450}
451
452impl From<ValueWireV1> for ValueContainer {
453 /// Unwraps the runtime container from a V1 DTO.
454 ///
455 /// # Parameters
456 ///
457 /// * `value` - Owned DTO whose container is moved into the result.
458 ///
459 /// # Returns
460 ///
461 /// The preserved runtime container without cloning its payload.
462 #[inline(always)]
463 fn from(value: ValueWireV1) -> Self {
464 value.into_container()
465 }
466}
467
468impl Serialize for ValueWireV1 {
469 /// Serializes the contained runtime shape through the V1 contract.
470 ///
471 /// # Type Parameters
472 ///
473 /// * `S` - Serde serializer defining the output format and error type.
474 ///
475 /// # Parameters
476 ///
477 /// * `serializer` - Serializer consumed to emit the versioned envelope.
478 ///
479 /// # Returns
480 ///
481 /// The serializer's completed output on success.
482 ///
483 /// # Errors
484 ///
485 /// Returns the serializer's error if it cannot encode the envelope or
486 /// payload.
487 #[inline(always)]
488 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
489 where
490 S: Serializer,
491 {
492 serialize_wire(self.value.container().into(), serializer)
493 }
494}