qubit_value/named_value.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//! # Named Single Value
9//!
10//! Provides a named container for single values, allowing readable identifiers
11//! to be added to individual values in complex configurations or structures.
12//!
13//! Suitable for scenarios such as log annotation, configuration item
14//! encapsulation, and preserving strongly typed values in key-value pairs.
15
16#[cfg(feature = "json")]
17use std::io::Write;
18
19#[cfg(feature = "json")]
20use qubit_budget::json::JsonDecodeLimits;
21#[cfg(feature = "json")]
22use qubit_budget::json::JsonDecodeSession;
23#[cfg(feature = "json")]
24use qubit_budget::json::JsonEncodeLimits;
25#[cfg(feature = "json")]
26use qubit_budget::json::JsonEncodeSession;
27#[cfg(feature = "json")]
28use qubit_json::decode::JsonDecoder;
29#[cfg(feature = "json")]
30use qubit_json::encode::JsonEncoder;
31use serde::Deserialize;
32use serde::Deserializer;
33use serde::Serialize;
34use serde::Serializer;
35use serde::de::Error as DeserializeError;
36use serde::ser::Error as SerializeError;
37
38use self::internal::NamedValueWireOwned;
39use self::internal::NamedValueWireRef;
40use super::value::Value;
41#[cfg(feature = "json")]
42use crate::ValueWireDecodeError;
43#[cfg(feature = "json")]
44use crate::ValueWireEncodeError;
45#[cfg(feature = "json")]
46use crate::ValueWireEncodePreflight;
47use crate::ValueWireRefV1;
48#[cfg(feature = "json")]
49use crate::ValueWireV1;
50
51mod internal;
52
53/// Named single value
54///
55/// Associates a human-readable name with a single [`Value`], facilitating
56/// identification, retrieval, and display in configurations, parameter passing,
57/// and complex data structures.
58///
59/// # Features
60///
61/// - Provides stable name identification for values
62/// - Exposes the inner [`Value`] through explicit accessors
63/// - Supports `serde` serialization and deserialization
64///
65/// # Use Cases
66///
67/// - Configuration item encapsulation (e.g., `"port"`, `"timeout"`, etc.)
68/// - Named output of key values in logs/monitoring
69/// - Quick location by name in collections
70///
71/// # Deserialization boundaries
72///
73/// [`Deserialize`] validates the V1 wire schema and requires a scalar payload,
74/// but it does not create a resource budget by itself. For a complete,
75/// untrusted JSON document, use `NamedValue::decode_json_slice` or
76/// `NamedValue::decode_json_slice_with_limits`. When this type is embedded in a
77/// larger document, deserialize it through a resource-bounded outer decoder.
78/// Bounded helpers report target-deserialization failures as `InvalidJson`
79/// metadata containing only the Serde category and location. Generic Serde
80/// errors retain the surrounding deserializer's semantics and may include
81/// input-derived text, such as field names. Reader and writer I/O error sources
82/// may also contain application-defined details. Resource limits do not
83/// sanitize diagnostics; apply the application's logging policy to all error
84/// sources.
85///
86/// # Examples
87///
88/// ```rust
89/// use qubit_value::{NamedValue, Value};
90///
91/// let named = NamedValue::new("flag", Value::Bool(true));
92/// assert!(named.value().get_bool().unwrap());
93/// ```
94///
95/// The wrapper intentionally does not forward [`Value`] methods implicitly:
96///
97/// ```compile_fail
98/// use qubit_value::{NamedValue, Value};
99///
100/// let named = NamedValue::new("flag", Value::Bool(true));
101/// let _ = named.get_bool();
102/// ```
103#[must_use]
104#[derive(Debug, Clone, PartialEq, Eq, Hash)]
105pub struct NamedValue {
106 /// Owned label, stored independently of the value and its runtime type.
107 name: String,
108 /// Owned scalar payload, retaining its runtime type and unset state.
109 value: Value,
110}
111
112impl NamedValue {
113 /// Create a new named value
114 ///
115 /// Converts the name into owned storage and moves `value` into the wrapper
116 /// without cloning it. The name is not required to be unique or nonempty.
117 ///
118 /// # Type Parameters
119 ///
120 /// * `impl Into<String>` - Name source converted into owned storage.
121 ///
122 /// # Parameters
123 ///
124 /// * `name` - Label converted into an owned string.
125 /// * `value` - Scalar payload whose ownership is transferred to the
126 /// wrapper.
127 ///
128 /// # Returns
129 ///
130 /// Returns a newly created [`NamedValue`] instance
131 ///
132 /// # Examples
133 ///
134 /// ```rust
135 /// use qubit_value::{NamedValue, Value};
136 ///
137 /// let named = NamedValue::new("timeout", Value::Int32(30));
138 /// assert_eq!(named.name(), "timeout");
139 /// ```
140 #[inline(always)]
141 pub fn new(name: impl Into<String>, value: Value) -> Self {
142 Self {
143 name: name.into(),
144 value,
145 }
146 }
147
148 /// Decodes a complete named scalar JSON document with default limits.
149 ///
150 /// # Parameters
151 ///
152 /// * `input` - Complete UTF-8 JSON document to decode.
153 ///
154 /// # Returns
155 ///
156 /// The decoded named scalar.
157 ///
158 /// # Errors
159 ///
160 /// Returns a JSON, wire-contract, or resource-limit error.
161 #[cfg(feature = "json")]
162 #[inline]
163 #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
164 pub fn decode_json_slice(input: &[u8]) -> Result<Self, ValueWireDecodeError> {
165 Self::decode_json_slice_with_limits(input, ValueWireV1::default_json_decode_limits())
166 }
167
168 /// Decodes a complete named scalar JSON document with explicit limits.
169 ///
170 /// The wrapper name and nested scalar share one accounting session.
171 ///
172 /// # Parameters
173 ///
174 /// * `input` - Complete UTF-8 JSON document to decode.
175 /// * `limits` - Input and decoded-resource limits.
176 ///
177 /// # Returns
178 ///
179 /// The decoded named scalar.
180 ///
181 /// # Errors
182 ///
183 /// Returns a JSON, wire-contract, or resource-limit error.
184 #[cfg(feature = "json")]
185 #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
186 pub fn decode_json_slice_with_limits(
187 input: &[u8],
188 limits: JsonDecodeLimits,
189 ) -> Result<Self, ValueWireDecodeError> {
190 let session = JsonDecodeSession::from_limits(limits);
191 JsonDecoder::new(session)
192 .decode_unique_utf8(input)
193 .map_err(ValueWireDecodeError::from)
194 }
195
196 /// Encodes this named scalar into a bounded compact JSON vector with the
197 /// default V1 JSON resource profile.
198 ///
199 /// # Returns
200 ///
201 /// Compact UTF-8 JSON bytes for the complete named scalar.
202 ///
203 /// # Errors
204 ///
205 /// Returns [`ValueWireEncodeError`] for resource or serialization failures.
206 #[cfg(feature = "json")]
207 #[inline]
208 #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
209 pub fn to_json_vec(&self) -> Result<Vec<u8>, ValueWireEncodeError> {
210 self.to_json_vec_with_limits(ValueWireV1::default_json_encode_limits())
211 }
212
213 /// Encodes this named scalar into a bounded compact JSON vector.
214 ///
215 /// # Parameters
216 ///
217 /// * `limits` - Resource limits enforced during JSON encoding.
218 ///
219 /// # Returns
220 ///
221 /// Compact UTF-8 JSON bytes for the complete named scalar.
222 ///
223 /// # Errors
224 ///
225 /// Returns [`ValueWireEncodeError`] when encoding exceeds `limits` or the
226 /// named scalar cannot be serialized.
227 #[cfg(feature = "json")]
228 #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
229 pub fn to_json_vec_with_limits(
230 &self,
231 limits: JsonEncodeLimits,
232 ) -> Result<Vec<u8>, ValueWireEncodeError> {
233 ValueWireEncodePreflight::new(limits).check_value(self.value())?;
234 let session = JsonEncodeSession::from_limits(limits);
235 JsonEncoder::new(session)
236 .to_vec(self)
237 .map_err(ValueWireEncodeError::from)
238 }
239
240 /// Encodes this named scalar to a writer with the default V1 JSON profile.
241 ///
242 /// # Type Parameters
243 ///
244 /// * `W` - Destination writer type.
245 ///
246 /// # Parameters
247 ///
248 /// * `writer` - Destination receiving the complete named scalar document.
249 ///
250 /// # Returns
251 ///
252 /// `Ok(())` after the complete document is written.
253 ///
254 /// The complete document is buffered and admitted before this method writes
255 /// its bytes to the destination. Preflight, serialization, or budget
256 /// rejection does not write this document. An I/O error may leave an
257 /// accepted prefix in the writer; do not blindly retry a complete
258 /// document on the same destination.
259 ///
260 /// This method does not explicitly flush the writer. Success means the
261 /// writer accepted all document bytes, not that they were flushed,
262 /// persisted, or received by a remote peer. Pass `&mut writer` to
263 /// retain ownership of a buffered writer, then call `flush()` and
264 /// handle its error separately. Passing an owned writer may invoke that
265 /// writer's drop behavior; errors from an implicit drop-time flush are
266 /// not returned by this method.
267 ///
268 /// # Errors
269 ///
270 /// Returns [`ValueWireEncodeError`] for resource, serialization, or writer
271 /// failures.
272 #[cfg(feature = "json")]
273 #[inline]
274 #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
275 pub fn to_json_writer<W>(&self, writer: W) -> Result<(), ValueWireEncodeError>
276 where
277 W: Write,
278 {
279 self.to_json_writer_with_limits(writer, ValueWireV1::default_json_encode_limits())
280 }
281
282 /// Encodes this named scalar to a writer after enforcing JSON budgets.
283 ///
284 /// # Type Parameters
285 ///
286 /// * `W` - Destination writer type.
287 ///
288 /// # Parameters
289 ///
290 /// * `writer` - Destination receiving the complete named scalar document.
291 /// * `limits` - Resource limits enforced during JSON encoding.
292 ///
293 /// # Returns
294 ///
295 /// `Ok(())` after the complete document is written.
296 ///
297 /// The complete document is buffered and admitted before this method writes
298 /// its bytes to the destination. Preflight, serialization, or budget
299 /// rejection does not write this document. An I/O error may leave an
300 /// accepted prefix in the writer; do not blindly retry a complete
301 /// document on the same destination.
302 ///
303 /// This method does not explicitly flush the writer. Success means the
304 /// writer accepted all document bytes, not that they were flushed,
305 /// persisted, or received by a remote peer. Pass `&mut writer` to
306 /// retain ownership of a buffered writer, then call `flush()` and
307 /// handle its error separately. Passing an owned writer may invoke that
308 /// writer's drop behavior; errors from an implicit drop-time flush are
309 /// not returned by this method.
310 ///
311 /// # Errors
312 ///
313 /// Returns [`ValueWireEncodeError`] when encoding exceeds `limits`, the
314 /// named scalar cannot be serialized, or `writer` rejects output.
315 #[cfg(feature = "json")]
316 #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
317 pub fn to_json_writer_with_limits<W>(
318 &self,
319 mut writer: W,
320 limits: JsonEncodeLimits,
321 ) -> Result<(), ValueWireEncodeError>
322 where
323 W: Write,
324 {
325 ValueWireEncodePreflight::new(limits).check_value(self.value())?;
326 let session = JsonEncodeSession::from_limits(limits);
327 JsonEncoder::new(session)
328 .write_buffered(&mut writer, self)
329 .map_err(ValueWireEncodeError::from)
330 }
331
332 /// Get a reference to the name
333 ///
334 /// Borrows the stored label without allocating. The slice remains valid
335 /// while this wrapper is shared-borrowed, preventing name replacement
336 /// through the wrapper during that borrow.
337 ///
338 /// # Returns
339 ///
340 /// Returns a string slice `&str` of the name
341 ///
342 /// # Examples
343 ///
344 /// ```rust
345 /// use qubit_value::{NamedValue, Value};
346 ///
347 /// let named = NamedValue::new("host", Value::String("localhost".to_string()));
348 /// assert_eq!(named.name(), "host");
349 /// ```
350 #[inline(always)]
351 #[must_use = "the borrowed name should be used"]
352 pub fn name(&self) -> &str {
353 &self.name
354 }
355
356 /// Borrows the contained value without cloning its payload.
357 ///
358 /// The reference is tied to the shared borrow of this wrapper; mutation
359 /// through the wrapper requires that borrow to end.
360 ///
361 /// # Returns
362 ///
363 /// A shared reference to the contained [`Value`].
364 #[inline(always)]
365 #[must_use = "the borrowed value should be used"]
366 pub fn value(&self) -> &Value {
367 &self.value
368 }
369
370 /// Borrows the contained value exclusively, allowing in-place updates.
371 ///
372 /// The reference is tied to the exclusive borrow of this wrapper. Updates
373 /// can change the payload or its runtime type without changing the name.
374 ///
375 /// # Returns
376 ///
377 /// An exclusive reference to the contained [`Value`].
378 #[inline]
379 #[must_use = "the mutable value reference should be used"]
380 pub fn value_mut(&mut self) -> &mut Value {
381 &mut self.value
382 }
383
384 /// Set a new name
385 ///
386 /// Converts the new label into owned storage and drops the previous name,
387 /// leaving the contained value unchanged. Empty and duplicate labels are
388 /// accepted without validation.
389 ///
390 /// # Type Parameters
391 ///
392 /// * `impl Into<String>` - Name source converted into owned storage.
393 ///
394 /// # Parameters
395 ///
396 /// * `name` - The new name
397 ///
398 /// # Examples
399 ///
400 /// ```rust
401 /// use qubit_value::{NamedValue, Value};
402 ///
403 /// let mut named = NamedValue::new("old_name", Value::Bool(true));
404 /// named.set_name("new_name");
405 /// assert_eq!(named.name(), "new_name");
406 /// ```
407 #[inline(always)]
408 pub fn set_name(&mut self, name: impl Into<String>) {
409 self.name = name.into();
410 }
411
412 /// Moves a new value into this wrapper and drops the previous payload.
413 ///
414 /// The replacement may change the runtime type or unset state; the stored
415 /// name is preserved and the new payload is not cloned.
416 ///
417 /// # Parameters
418 ///
419 /// * `value` - New value to store under the existing name.
420 #[inline]
421 pub fn set_value(&mut self, value: Value) {
422 self.value = value;
423 }
424
425 /// Consumes this wrapper and returns its owned name and value.
426 ///
427 /// # Returns
428 ///
429 /// The `(name, value)` pair without cloning either component.
430 #[inline]
431 #[must_use = "consuming NamedValue without using its parts loses both fields"]
432 pub fn into_parts(self) -> (String, Value) {
433 (self.name, self.value)
434 }
435}
436
437impl Serialize for NamedValue {
438 /// Serializes the name and its explicitly versioned scalar value.
439 ///
440 /// # Type Parameters
441 ///
442 /// * `S` - Serde serializer that determines the output format.
443 ///
444 /// # Parameters
445 ///
446 /// * `serializer` - Destination serializer receiving the named V1 scalar
447 /// envelope.
448 ///
449 /// # Returns
450 ///
451 /// The serializer's success value after serializing the complete envelope.
452 ///
453 /// # Errors
454 ///
455 /// Returns `S::Error` when the scalar cannot be represented by the V1 wire
456 /// contract or the serializer rejects the envelope.
457 #[inline]
458 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
459 where
460 S: Serializer,
461 {
462 let value = ValueWireRefV1::try_from(self.value()).map_err(SerializeError::custom)?;
463 NamedValueWireRef {
464 name: self.name(),
465 value,
466 }
467 .serialize(serializer)
468 }
469}
470
471impl<'de> Deserialize<'de> for NamedValue {
472 /// Deserializes a named scalar value from the V1 wire contract.
473 ///
474 /// This implementation validates the wire schema and scalar shape, but
475 /// inherits resource accounting from `deserializer`. Callers handling a
476 /// complete, untrusted JSON document should use the bounded JSON helpers on
477 /// [`NamedValue`] instead of an unbounded Serde entry point.
478 /// Generic deserializer errors may contain input-derived text. The bounded
479 /// helpers instead expose only Serde category and location for target
480 /// deserialization failures; resource limits do not sanitize other errors.
481 ///
482 /// # Type Parameters
483 ///
484 /// * `D` - Serde deserializer that supplies the input and any outer budget.
485 ///
486 /// # Parameters
487 ///
488 /// * `deserializer` - Source containing one named V1 scalar envelope.
489 ///
490 /// # Returns
491 ///
492 /// The decoded name and scalar value.
493 ///
494 /// # Errors
495 ///
496 /// Returns `D::Error` for an invalid V1 envelope, an unsupported payload,
497 /// or a payload whose shape is not scalar.
498 #[inline]
499 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
500 where
501 D: Deserializer<'de>,
502 {
503 let NamedValueWireOwned { name, value } = NamedValueWireOwned::deserialize(deserializer)?;
504 let value = value.into_container().into_scalar().map_err(|_| {
505 DeserializeError::custom("named value wire payload must contain a scalar")
506 })?;
507 Ok(Self::new(name, value))
508 }
509}