Skip to main content

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(input: &[u8], limits: JsonDecodeLimits) -> Result<Self, ValueWireDecodeError> {
187        let session = JsonDecodeSession::from_limits(limits);
188        JsonDecoder::new(session)
189            .decode_unique_utf8(input)
190            .map_err(ValueWireDecodeError::from)
191    }
192
193    /// Encodes this named scalar into a bounded compact JSON vector with the
194    /// default V1 JSON resource profile.
195    ///
196    /// # Returns
197    ///
198    /// Compact UTF-8 JSON bytes for the complete named scalar.
199    ///
200    /// # Errors
201    ///
202    /// Returns [`ValueWireEncodeError`] for resource or serialization failures.
203    #[cfg(feature = "json")]
204    #[inline]
205    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
206    pub fn to_json_vec(&self) -> Result<Vec<u8>, ValueWireEncodeError> {
207        self.to_json_vec_with_limits(ValueWireV1::default_json_encode_limits())
208    }
209
210    /// Encodes this named scalar into a bounded compact JSON vector.
211    ///
212    /// # Parameters
213    ///
214    /// * `limits` - Resource limits enforced during JSON encoding.
215    ///
216    /// # Returns
217    ///
218    /// Compact UTF-8 JSON bytes for the complete named scalar.
219    ///
220    /// # Errors
221    ///
222    /// Returns [`ValueWireEncodeError`] when encoding exceeds `limits` or the
223    /// named scalar cannot be serialized.
224    #[cfg(feature = "json")]
225    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
226    pub fn to_json_vec_with_limits(&self, limits: JsonEncodeLimits) -> Result<Vec<u8>, ValueWireEncodeError> {
227        ValueWireEncodePreflight::new(limits).check_value(self.value())?;
228        let session = JsonEncodeSession::from_limits(limits);
229        JsonEncoder::new(session)
230            .to_vec(self)
231            .map_err(ValueWireEncodeError::from)
232    }
233
234    /// Encodes this named scalar to a writer with the default V1 JSON profile.
235    ///
236    /// # Type Parameters
237    ///
238    /// * `W` - Destination writer type.
239    ///
240    /// # Parameters
241    ///
242    /// * `writer` - Destination receiving the complete named scalar document.
243    ///
244    /// # Returns
245    ///
246    /// `Ok(())` after the complete document is written.
247    ///
248    /// The complete document is buffered and admitted before this method writes
249    /// its bytes to the destination. Preflight, serialization, or budget
250    /// rejection does not write this document. An I/O error may leave an
251    /// accepted prefix in the writer; do not blindly retry a complete
252    /// document on the same destination.
253    ///
254    /// This method does not explicitly flush the writer. Success means the
255    /// writer accepted all document bytes, not that they were flushed,
256    /// persisted, or received by a remote peer. Pass `&mut writer` to
257    /// retain ownership of a buffered writer, then call `flush()` and
258    /// handle its error separately. Passing an owned writer may invoke that
259    /// writer's drop behavior; errors from an implicit drop-time flush are
260    /// not returned by this method.
261    ///
262    /// # Errors
263    ///
264    /// Returns [`ValueWireEncodeError`] for resource, serialization, or writer
265    /// failures.
266    #[cfg(feature = "json")]
267    #[inline]
268    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
269    pub fn to_json_writer<W>(&self, writer: W) -> Result<(), ValueWireEncodeError>
270    where
271        W: Write,
272    {
273        self.to_json_writer_with_limits(writer, ValueWireV1::default_json_encode_limits())
274    }
275
276    /// Encodes this named scalar to a writer after enforcing JSON budgets.
277    ///
278    /// # Type Parameters
279    ///
280    /// * `W` - Destination writer type.
281    ///
282    /// # Parameters
283    ///
284    /// * `writer` - Destination receiving the complete named scalar document.
285    /// * `limits` - Resource limits enforced during JSON encoding.
286    ///
287    /// # Returns
288    ///
289    /// `Ok(())` after the complete document is written.
290    ///
291    /// The complete document is buffered and admitted before this method writes
292    /// its bytes to the destination. Preflight, serialization, or budget
293    /// rejection does not write this document. An I/O error may leave an
294    /// accepted prefix in the writer; do not blindly retry a complete
295    /// document on the same destination.
296    ///
297    /// This method does not explicitly flush the writer. Success means the
298    /// writer accepted all document bytes, not that they were flushed,
299    /// persisted, or received by a remote peer. Pass `&mut writer` to
300    /// retain ownership of a buffered writer, then call `flush()` and
301    /// handle its error separately. Passing an owned writer may invoke that
302    /// writer's drop behavior; errors from an implicit drop-time flush are
303    /// not returned by this method.
304    ///
305    /// # Errors
306    ///
307    /// Returns [`ValueWireEncodeError`] when encoding exceeds `limits`, the
308    /// named scalar cannot be serialized, or `writer` rejects output.
309    #[cfg(feature = "json")]
310    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
311    pub fn to_json_writer_with_limits<W>(
312        &self,
313        mut writer: W,
314        limits: JsonEncodeLimits,
315    ) -> Result<(), ValueWireEncodeError>
316    where
317        W: Write,
318    {
319        ValueWireEncodePreflight::new(limits).check_value(self.value())?;
320        let session = JsonEncodeSession::from_limits(limits);
321        JsonEncoder::new(session)
322            .write_buffered(&mut writer, self)
323            .map_err(ValueWireEncodeError::from)
324    }
325
326    /// Get a reference to the name
327    ///
328    /// Borrows the stored label without allocating. The slice remains valid
329    /// while this wrapper is shared-borrowed, preventing name replacement
330    /// through the wrapper during that borrow.
331    ///
332    /// # Returns
333    ///
334    /// Returns a string slice `&str` of the name
335    ///
336    /// # Examples
337    ///
338    /// ```rust
339    /// use qubit_value::{NamedValue, Value};
340    ///
341    /// let named = NamedValue::new("host", Value::String("localhost".to_string()));
342    /// assert_eq!(named.name(), "host");
343    /// ```
344    #[inline(always)]
345    #[must_use = "the borrowed name should be used"]
346    pub fn name(&self) -> &str {
347        &self.name
348    }
349
350    /// Borrows the contained value without cloning its payload.
351    ///
352    /// The reference is tied to the shared borrow of this wrapper; mutation
353    /// through the wrapper requires that borrow to end.
354    ///
355    /// # Returns
356    ///
357    /// A shared reference to the contained [`Value`].
358    #[inline(always)]
359    #[must_use = "the borrowed value should be used"]
360    pub fn value(&self) -> &Value {
361        &self.value
362    }
363
364    /// Borrows the contained value exclusively, allowing in-place updates.
365    ///
366    /// The reference is tied to the exclusive borrow of this wrapper. Updates
367    /// can change the payload or its runtime type without changing the name.
368    ///
369    /// # Returns
370    ///
371    /// An exclusive reference to the contained [`Value`].
372    #[inline]
373    #[must_use = "the mutable value reference should be used"]
374    pub fn value_mut(&mut self) -> &mut Value {
375        &mut self.value
376    }
377
378    /// Set a new name
379    ///
380    /// Converts the new label into owned storage and drops the previous name,
381    /// leaving the contained value unchanged. Empty and duplicate labels are
382    /// accepted without validation.
383    ///
384    /// # Type Parameters
385    ///
386    /// * `impl Into<String>` - Name source converted into owned storage.
387    ///
388    /// # Parameters
389    ///
390    /// * `name` - The new name
391    ///
392    /// # Examples
393    ///
394    /// ```rust
395    /// use qubit_value::{NamedValue, Value};
396    ///
397    /// let mut named = NamedValue::new("old_name", Value::Bool(true));
398    /// named.set_name("new_name");
399    /// assert_eq!(named.name(), "new_name");
400    /// ```
401    #[inline(always)]
402    pub fn set_name(&mut self, name: impl Into<String>) {
403        self.name = name.into();
404    }
405
406    /// Moves a new value into this wrapper and drops the previous payload.
407    ///
408    /// The replacement may change the runtime type or unset state; the stored
409    /// name is preserved and the new payload is not cloned.
410    ///
411    /// # Parameters
412    ///
413    /// * `value` - New value to store under the existing name.
414    #[inline]
415    pub fn set_value(&mut self, value: Value) {
416        self.value = value;
417    }
418
419    /// Consumes this wrapper and returns its owned name and value.
420    ///
421    /// # Returns
422    ///
423    /// The `(name, value)` pair without cloning either component.
424    #[inline]
425    #[must_use = "consuming NamedValue without using its parts loses both fields"]
426    pub fn into_parts(self) -> (String, Value) {
427        (self.name, self.value)
428    }
429}
430
431impl Serialize for NamedValue {
432    /// Serializes the name and its explicitly versioned scalar value.
433    ///
434    /// # Type Parameters
435    ///
436    /// * `S` - Serde serializer that determines the output format.
437    ///
438    /// # Parameters
439    ///
440    /// * `serializer` - Destination serializer receiving the named V1 scalar
441    ///   envelope.
442    ///
443    /// # Returns
444    ///
445    /// The serializer's success value after serializing the complete envelope.
446    ///
447    /// # Errors
448    ///
449    /// Returns `S::Error` when the scalar cannot be represented by the V1 wire
450    /// contract or the serializer rejects the envelope.
451    #[inline]
452    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
453    where
454        S: Serializer,
455    {
456        let value = ValueWireRefV1::try_from(self.value()).map_err(SerializeError::custom)?;
457        NamedValueWireRef {
458            name: self.name(),
459            value,
460        }
461        .serialize(serializer)
462    }
463}
464
465impl<'de> Deserialize<'de> for NamedValue {
466    /// Deserializes a named scalar value from the V1 wire contract.
467    ///
468    /// This implementation validates the wire schema and scalar shape, but
469    /// inherits resource accounting from `deserializer`. Callers handling a
470    /// complete, untrusted JSON document should use the bounded JSON helpers on
471    /// [`NamedValue`] instead of an unbounded Serde entry point.
472    /// Generic deserializer errors may contain input-derived text. The bounded
473    /// helpers instead expose only Serde category and location for target
474    /// deserialization failures; resource limits do not sanitize other errors.
475    ///
476    /// # Type Parameters
477    ///
478    /// * `D` - Serde deserializer that supplies the input and any outer budget.
479    ///
480    /// # Parameters
481    ///
482    /// * `deserializer` - Source containing one named V1 scalar envelope.
483    ///
484    /// # Returns
485    ///
486    /// The decoded name and scalar value.
487    ///
488    /// # Errors
489    ///
490    /// Returns `D::Error` for an invalid V1 envelope, an unsupported payload,
491    /// or a payload whose shape is not scalar.
492    #[inline]
493    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
494    where
495        D: Deserializer<'de>,
496    {
497        let NamedValueWireOwned { name, value } = NamedValueWireOwned::deserialize(deserializer)?;
498        let value = value
499            .into_container()
500            .into_scalar()
501            .map_err(|_| DeserializeError::custom("named value wire payload must contain a scalar"))?;
502        Ok(Self::new(name, value))
503    }
504}