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(
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}