Skip to main content

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