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