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 super::value::Value;
39#[cfg(feature = "json")]
40use crate::ValueWireDecodeError;
41#[cfg(feature = "json")]
42use crate::ValueWireEncodeError;
43use crate::ValueWireRefV1;
44#[cfg(feature = "json")]
45use crate::ValueWireV1;
46
47mod internal;
48
49use self::internal::NamedValueWireOwned;
50use self::internal::NamedValueWireRef;
51
52/// Named single value
53///
54/// Associates a human-readable name with a single [`Value`], facilitating
55/// identification, retrieval, and display in configurations, parameter passing,
56/// and complex data structures.
57///
58/// # Features
59///
60/// - Provides stable name identification for values
61/// - Exposes the inner [`Value`] through explicit accessors
62/// - Supports `serde` serialization and deserialization
63///
64/// # Use Cases
65///
66/// - Configuration item encapsulation (e.g., `"port"`, `"timeout"`, etc.)
67/// - Named output of key values in logs/monitoring
68/// - Quick location by name in collections
69///
70/// # Deserialization boundaries
71///
72/// [`Deserialize`] validates the V1 wire schema and requires a scalar payload,
73/// but it does not create a resource budget by itself. For a complete,
74/// untrusted JSON document, use `NamedValue::decode_json_slice` or
75/// `NamedValue::decode_json_slice_with_limits`. When this type is embedded in a
76/// larger document, deserialize it through a resource-bounded outer decoder.
77///
78/// # Examples
79///
80/// ```rust
81/// use qubit_value::{NamedValue, Value};
82///
83/// let named = NamedValue::new("flag", Value::Bool(true));
84/// assert!(named.value().get_bool().unwrap());
85/// ```
86///
87/// The wrapper intentionally does not forward [`Value`] methods implicitly:
88///
89/// ```compile_fail
90/// use qubit_value::{NamedValue, Value};
91///
92/// let named = NamedValue::new("flag", Value::Bool(true));
93/// let _ = named.get_bool();
94/// ```
95#[must_use]
96#[derive(Debug, Clone, PartialEq, Eq, Hash)]
97pub struct NamedValue {
98    /// Name of the value
99    name: String,
100    /// Content of the value
101    value: Value,
102}
103
104impl NamedValue {
105    /// Create a new named value
106    ///
107    /// Creates a binding instance between a name and a value.
108    ///
109    /// # Type Parameters
110    ///
111    /// * `impl Into<String>` - Name source converted into owned storage.
112    ///
113    /// # Parameters
114    ///
115    /// * `name` - Name of the value
116    /// * `value` - Content of the value
117    ///
118    /// # Returns
119    ///
120    /// Returns a newly created [`NamedValue`] instance
121    ///
122    /// # Examples
123    ///
124    /// ```rust
125    /// use qubit_value::{NamedValue, Value};
126    ///
127    /// let named = NamedValue::new("timeout", Value::Int32(30));
128    /// assert_eq!(named.name(), "timeout");
129    /// ```
130    #[inline(always)]
131    pub fn new(name: impl Into<String>, value: Value) -> Self {
132        Self {
133            name: name.into(),
134            value,
135        }
136    }
137
138    /// Decodes a complete named scalar JSON document with default limits.
139    ///
140    /// # Parameters
141    ///
142    /// * `input` - Complete UTF-8 JSON document to decode.
143    ///
144    /// # Returns
145    ///
146    /// The decoded named scalar.
147    ///
148    /// # Errors
149    ///
150    /// Returns a JSON, wire-contract, or resource-limit error.
151    #[cfg(feature = "json")]
152    #[inline(always)]
153    pub fn decode_json_slice(input: &[u8]) -> Result<Self, ValueWireDecodeError> {
154        Self::decode_json_slice_with_limits(input, ValueWireV1::default_json_decode_limits())
155    }
156
157    /// Decodes a complete named scalar JSON document with explicit limits.
158    ///
159    /// The wrapper name and nested scalar share one accounting session.
160    ///
161    /// # Parameters
162    ///
163    /// * `input` - Complete UTF-8 JSON document to decode.
164    /// * `limits` - Input and decoded-resource limits.
165    ///
166    /// # Returns
167    ///
168    /// The decoded named scalar.
169    ///
170    /// # Errors
171    ///
172    /// Returns a JSON, wire-contract, or resource-limit error.
173    #[cfg(feature = "json")]
174    pub fn decode_json_slice_with_limits(input: &[u8], limits: JsonDecodeLimits) -> Result<Self, ValueWireDecodeError> {
175        let session = JsonDecodeSession::from_limits(limits);
176        JsonDecoder::new(session)
177            .decode_utf8(input)
178            .map_err(ValueWireDecodeError::from)
179    }
180
181    /// Encodes this named scalar into a bounded compact JSON vector with the
182    /// default V1 JSON resource profile.
183    ///
184    /// # Returns
185    ///
186    /// Compact UTF-8 JSON bytes for the complete named scalar.
187    ///
188    /// # Errors
189    ///
190    /// Returns [`ValueWireEncodeError`] for resource or serialization failures.
191    #[cfg(feature = "json")]
192    #[inline(always)]
193    pub fn to_json_vec(&self) -> Result<Vec<u8>, ValueWireEncodeError> {
194        self.to_json_vec_with_limits(ValueWireV1::default_json_encode_limits())
195    }
196
197    /// Encodes this named scalar into a bounded compact JSON vector.
198    ///
199    /// # Parameters
200    ///
201    /// * `limits` - Resource limits enforced during JSON encoding.
202    ///
203    /// # Returns
204    ///
205    /// Compact UTF-8 JSON bytes for the complete named scalar.
206    ///
207    /// # Errors
208    ///
209    /// Returns [`ValueWireEncodeError`] when encoding exceeds `limits` or the
210    /// named scalar cannot be serialized.
211    #[cfg(feature = "json")]
212    pub fn to_json_vec_with_limits(&self, limits: JsonEncodeLimits) -> Result<Vec<u8>, ValueWireEncodeError> {
213        let session = JsonEncodeSession::from_limits(limits);
214        JsonEncoder::new(session)
215            .to_vec(self)
216            .map_err(ValueWireEncodeError::from)
217    }
218
219    /// Encodes this named scalar to a writer with the default V1 JSON profile.
220    ///
221    /// # Type Parameters
222    ///
223    /// * `W` - Destination writer type.
224    ///
225    /// # Parameters
226    ///
227    /// * `writer` - Destination receiving the complete named scalar document.
228    ///
229    /// # Returns
230    ///
231    /// `Ok(())` after the complete document is written.
232    ///
233    /// # Errors
234    ///
235    /// Returns [`ValueWireEncodeError`] for resource, serialization, or writer
236    /// failures.
237    #[cfg(feature = "json")]
238    #[inline(always)]
239    pub fn to_json_writer<W>(&self, writer: W) -> Result<(), ValueWireEncodeError>
240    where
241        W: Write,
242    {
243        self.to_json_writer_with_limits(writer, ValueWireV1::default_json_encode_limits())
244    }
245
246    /// Encodes this named scalar to a writer after enforcing JSON budgets.
247    ///
248    /// # Type Parameters
249    ///
250    /// * `W` - Destination writer type.
251    ///
252    /// # Parameters
253    ///
254    /// * `writer` - Destination receiving the complete named scalar document.
255    /// * `limits` - Resource limits enforced during JSON encoding.
256    ///
257    /// # Returns
258    ///
259    /// `Ok(())` after the complete document is written.
260    ///
261    /// # Errors
262    ///
263    /// Returns [`ValueWireEncodeError`] when encoding exceeds `limits`, the
264    /// named scalar cannot be serialized, or `writer` rejects output.
265    #[cfg(feature = "json")]
266    pub fn to_json_writer_with_limits<W>(&self, writer: W, limits: JsonEncodeLimits) -> Result<(), ValueWireEncodeError>
267    where
268        W: Write,
269    {
270        let session = JsonEncodeSession::from_limits(limits);
271        JsonEncoder::new(session)
272            .write_buffered(writer, self)
273            .map_err(ValueWireEncodeError::from)
274    }
275
276    /// Get a reference to the name
277    ///
278    /// Returns a read-only name slice bound to this value.
279    ///
280    /// # Returns
281    ///
282    /// Returns a string slice `&str` of the name
283    ///
284    /// # Examples
285    ///
286    /// ```rust
287    /// use qubit_value::{NamedValue, Value};
288    ///
289    /// let named = NamedValue::new("host", Value::String("localhost".to_string()));
290    /// assert_eq!(named.name(), "host");
291    /// ```
292    #[inline(always)]
293    #[must_use = "the borrowed name should be used"]
294    pub fn name(&self) -> &str {
295        &self.name
296    }
297
298    /// Set a new name
299    ///
300    /// Updates the name bound to the current instance.
301    ///
302    /// # Type Parameters
303    ///
304    /// * `impl Into<String>` - Name source converted into owned storage.
305    ///
306    /// # Parameters
307    ///
308    /// * `name` - The new name
309    ///
310    /// # Examples
311    ///
312    /// ```rust
313    /// use qubit_value::{NamedValue, Value};
314    ///
315    /// let mut named = NamedValue::new("old_name", Value::Bool(true));
316    /// named.set_name("new_name");
317    /// assert_eq!(named.name(), "new_name");
318    /// ```
319    #[inline(always)]
320    pub fn set_name(&mut self, name: impl Into<String>) {
321        self.name = name.into();
322    }
323
324    /// Borrows the contained value.
325    ///
326    /// # Returns
327    ///
328    /// A shared reference to the contained [`Value`].
329    #[inline(always)]
330    #[must_use = "the borrowed value should be used"]
331    pub fn value(&self) -> &Value {
332        &self.value
333    }
334
335    /// Mutably borrows the contained value.
336    ///
337    /// # Returns
338    ///
339    /// An exclusive reference to the contained [`Value`].
340    #[inline(always)]
341    #[must_use = "the mutable value reference should be used"]
342    pub fn value_mut(&mut self) -> &mut Value {
343        &mut self.value
344    }
345
346    /// Replaces the contained value.
347    ///
348    /// # Parameters
349    ///
350    /// * `value` - New value to store under the existing name.
351    #[inline(always)]
352    pub fn set_value(&mut self, value: Value) {
353        self.value = value;
354    }
355
356    /// Consumes this wrapper and returns its owned name and value.
357    ///
358    /// # Returns
359    ///
360    /// The `(name, value)` pair without cloning either component.
361    #[inline(always)]
362    #[must_use = "consuming NamedValue without using its parts loses both fields"]
363    pub fn into_parts(self) -> (String, Value) {
364        (self.name, self.value)
365    }
366}
367
368impl Serialize for NamedValue {
369    /// Serializes the name and its explicitly versioned scalar value.
370    #[inline]
371    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
372    where
373        S: Serializer,
374    {
375        let value = ValueWireRefV1::try_from(self.value()).map_err(SerializeError::custom)?;
376        NamedValueWireRef {
377            name: self.name(),
378            value,
379        }
380        .serialize(serializer)
381    }
382}
383
384impl<'de> Deserialize<'de> for NamedValue {
385    /// Deserializes a named scalar value from the V1 wire contract.
386    ///
387    /// This implementation validates the wire schema and scalar shape, but
388    /// inherits resource accounting from `deserializer`. Callers handling a
389    /// complete, untrusted JSON document should use the bounded JSON helpers on
390    /// [`NamedValue`] instead of an unbounded Serde entry point.
391    ///
392    /// # Type Parameters
393    ///
394    /// * `D` - Serde deserializer that supplies the input and any outer budget.
395    ///
396    /// # Parameters
397    ///
398    /// * `deserializer` - Source containing one named V1 scalar envelope.
399    ///
400    /// # Returns
401    ///
402    /// The decoded name and scalar value.
403    ///
404    /// # Errors
405    ///
406    /// Returns `D::Error` for an invalid V1 envelope, an unsupported payload,
407    /// or a payload whose shape is not scalar.
408    #[inline]
409    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
410    where
411        D: Deserializer<'de>,
412    {
413        let NamedValueWireOwned { name, value } = NamedValueWireOwned::deserialize(deserializer)?;
414        let value = value
415            .into_container()
416            .into_scalar()
417            .map_err(|_| DeserializeError::custom("named value wire payload must contain a scalar"))?;
418        Ok(Self::new(name, value))
419    }
420}