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(
204        input: &[u8],
205        limits: JsonDecodeLimits,
206    ) -> Result<Self, ValueWireDecodeError> {
207        let session = JsonDecodeSession::from_limits(limits);
208        JsonDecoder::new(session)
209            .decode_unique_utf8(input)
210            .map_err(ValueWireDecodeError::from)
211    }
212
213    /// Encodes this named collection into a bounded compact JSON vector with
214    /// the default V1 JSON resource profile.
215    ///
216    /// # Returns
217    ///
218    /// Compact UTF-8 JSON bytes for the complete named collection.
219    ///
220    /// # Errors
221    ///
222    /// Returns [`ValueWireEncodeError`] for resource or serialization failures.
223    #[cfg(feature = "json")]
224    #[inline]
225    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
226    pub fn to_json_vec(&self) -> Result<Vec<u8>, ValueWireEncodeError> {
227        self.to_json_vec_with_limits(ValueWireV1::default_json_encode_limits())
228    }
229
230    /// Encodes this named collection into a bounded compact JSON vector.
231    ///
232    /// # Parameters
233    ///
234    /// * `limits` - Resource limits enforced during JSON encoding.
235    ///
236    /// # Returns
237    ///
238    /// Compact UTF-8 JSON bytes for the complete named collection.
239    ///
240    /// # Errors
241    ///
242    /// Returns [`ValueWireEncodeError`] when encoding exceeds `limits` or the
243    /// named collection cannot be serialized.
244    #[cfg(feature = "json")]
245    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
246    pub fn to_json_vec_with_limits(
247        &self,
248        limits: JsonEncodeLimits,
249    ) -> Result<Vec<u8>, ValueWireEncodeError> {
250        ValueWireEncodePreflight::new(limits).check_values(self.values())?;
251        let session = JsonEncodeSession::from_limits(limits);
252        JsonEncoder::new(session)
253            .to_vec(self)
254            .map_err(ValueWireEncodeError::from)
255    }
256
257    /// Encodes this named collection to a writer with the default V1 JSON
258    /// profile.
259    ///
260    /// # Type Parameters
261    ///
262    /// * `W` - Destination writer type.
263    ///
264    /// # Parameters
265    ///
266    /// * `writer` - Destination receiving the complete named collection.
267    ///
268    /// # Returns
269    ///
270    /// `Ok(())` after the complete document is written.
271    ///
272    /// The complete document is buffered and admitted before this method writes
273    /// its bytes to the destination. Preflight, serialization, or budget
274    /// rejection does not write this document. An I/O error may leave an
275    /// accepted prefix in the writer; do not blindly retry a complete
276    /// document on the same destination.
277    ///
278    /// This method does not explicitly flush the writer. Success means the
279    /// writer accepted all document bytes, not that they were flushed,
280    /// persisted, or received by a remote peer. Pass `&mut writer` to
281    /// retain ownership of a buffered writer, then call `flush()` and
282    /// handle its error separately. Passing an owned writer may invoke that
283    /// writer's drop behavior; errors from an implicit drop-time flush are
284    /// not returned by this method.
285    ///
286    /// # Errors
287    ///
288    /// Returns [`ValueWireEncodeError`] for resource, serialization, or writer
289    /// failures.
290    #[cfg(feature = "json")]
291    #[inline]
292    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
293    pub fn to_json_writer<W>(&self, writer: W) -> Result<(), ValueWireEncodeError>
294    where
295        W: Write,
296    {
297        self.to_json_writer_with_limits(writer, ValueWireV1::default_json_encode_limits())
298    }
299
300    /// Encodes this named collection to a writer after enforcing JSON budgets.
301    ///
302    /// # Type Parameters
303    ///
304    /// * `W` - Destination writer type.
305    ///
306    /// # Parameters
307    ///
308    /// * `writer` - Destination receiving the complete named collection.
309    /// * `limits` - Resource limits enforced during JSON encoding.
310    ///
311    /// # Returns
312    ///
313    /// `Ok(())` after the complete document is written.
314    ///
315    /// The complete document is buffered and admitted before this method writes
316    /// its bytes to the destination. Preflight, serialization, or budget
317    /// rejection does not write this document. An I/O error may leave an
318    /// accepted prefix in the writer; do not blindly retry a complete
319    /// document on the same destination.
320    ///
321    /// This method does not explicitly flush the writer. Success means the
322    /// writer accepted all document bytes, not that they were flushed,
323    /// persisted, or received by a remote peer. Pass `&mut writer` to
324    /// retain ownership of a buffered writer, then call `flush()` and
325    /// handle its error separately. Passing an owned writer may invoke that
326    /// writer's drop behavior; errors from an implicit drop-time flush are
327    /// not returned by this method.
328    ///
329    /// # Errors
330    ///
331    /// Returns [`ValueWireEncodeError`] when encoding exceeds `limits`, the
332    /// named collection cannot be serialized, or `writer` rejects output.
333    #[cfg(feature = "json")]
334    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
335    pub fn to_json_writer_with_limits<W>(
336        &self,
337        mut writer: W,
338        limits: JsonEncodeLimits,
339    ) -> Result<(), ValueWireEncodeError>
340    where
341        W: Write,
342    {
343        ValueWireEncodePreflight::new(limits).check_values(self.values())?;
344        let session = JsonEncodeSession::from_limits(limits);
345        JsonEncoder::new(session)
346            .write_buffered(&mut writer, self)
347            .map_err(ValueWireEncodeError::from)
348    }
349
350    /// Borrows the human-readable label without allocating or inspecting the
351    /// collection.
352    ///
353    /// The slice remains valid while this container is borrowed.
354    ///
355    /// # Returns
356    ///
357    /// Returns a string slice of the name
358    ///
359    /// # Examples
360    ///
361    /// ```rust
362    /// use qubit_value::{NamedMultiValues, MultiValues};
363    ///
364    /// let named = NamedMultiValues::new("items", MultiValues::Int32(vec![1, 2, 3]));
365    /// assert_eq!(named.name(), "items");
366    /// ```
367    #[inline(always)]
368    #[must_use = "the borrowed name should be used"]
369    pub fn name(&self) -> &str {
370        &self.name
371    }
372
373    /// Borrows the collection without cloning its elements or its label.
374    ///
375    /// # Returns
376    ///
377    /// A shared reference to the contained [`MultiValues`].
378    #[inline(always)]
379    #[must_use = "the borrowed values should be used"]
380    pub fn values(&self) -> &MultiValues {
381        &self.value
382    }
383
384    /// Exclusively borrows the collection so it can be changed without changing
385    /// the label.
386    ///
387    /// # Returns
388    ///
389    /// An exclusive reference to the contained [`MultiValues`].
390    #[inline]
391    #[must_use = "the mutable values reference should be used"]
392    pub fn values_mut(&mut self) -> &mut MultiValues {
393        &mut self.value
394    }
395
396    /// Convert this named multi-values into a named single value.
397    ///
398    /// The returned value keeps the same name and uses the first element from
399    /// the inner [`MultiValues`]. If there is no element, the returned value is
400    /// `Value::Unset` with the same data type.
401    ///
402    /// # Returns
403    ///
404    /// A named clone of the first item, or a named typed unset value.
405    #[inline]
406    pub fn first_named_value(&self) -> NamedValue {
407        NamedValue::new(self.name.as_str(), self.value.first_value())
408    }
409
410    /// Replaces the owned label while preserving the collection unchanged.
411    ///
412    /// Converts the supplied name into owned storage and drops the previous
413    /// label.
414    ///
415    /// # Type Parameters
416    ///
417    /// * `impl Into<String>` - Name source converted into owned storage.
418    ///
419    /// # Parameters
420    ///
421    /// * `name` - Human-readable label to associate with the existing
422    ///   collection.
423    ///
424    /// # Returns
425    ///
426    /// No return value
427    ///
428    /// # Examples
429    ///
430    /// ```rust
431    /// use qubit_value::{NamedMultiValues, MultiValues};
432    ///
433    /// let mut named = NamedMultiValues::new("old", MultiValues::Bool(vec![true]));
434    /// named.set_name("new");
435    /// assert_eq!(named.name(), "new");
436    /// ```
437    #[inline(always)]
438    pub fn set_name(&mut self, name: impl Into<String>) {
439        self.name = name.into();
440    }
441
442    /// Replaces the contained values.
443    ///
444    /// # Parameters
445    ///
446    /// * `values` - New collection to store under the existing name.
447    #[inline]
448    pub fn set_values(&mut self, values: MultiValues) {
449        self.value = values;
450    }
451
452    /// Consumes this wrapper and returns its owned name and values.
453    ///
454    /// # Returns
455    ///
456    /// The `(name, values)` pair without cloning either component.
457    #[inline(always)]
458    #[must_use = "consuming NamedMultiValues without using its parts loses both fields"]
459    pub fn into_parts(self) -> (String, MultiValues) {
460        (self.name, self.value)
461    }
462
463    /// Consumes this container and converts its first item to a named value.
464    ///
465    /// The owned name and first stored item are moved into the result. An empty
466    /// or unset collection produces [`crate::Value::Unset`] with the same data
467    /// type.
468    ///
469    /// # Returns
470    ///
471    /// A named owned first item, or a named typed unset value.
472    #[inline]
473    pub fn into_first_named_value(self) -> NamedValue {
474        let (name, values) = self.into_parts();
475        NamedValue::new(name, values.into_first_value())
476    }
477}
478
479impl From<NamedValue> for NamedMultiValues {
480    /// Construct `NamedMultiValues` from `NamedValue`
481    ///
482    /// Reuses the name and promotes the single value to a `MultiValues`
483    /// containing only one element.
484    ///
485    /// # Parameters
486    ///
487    /// * `named` - Named scalar consumed to transfer its name and value.
488    ///
489    /// # Returns
490    ///
491    /// A named collection containing the promoted scalar.
492    #[inline]
493    fn from(named: NamedValue) -> Self {
494        let (name, value) = named.into_parts();
495        let value = MultiValues::from(value);
496        Self { name, value }
497    }
498}
499
500impl Serialize for NamedMultiValues {
501    /// Serializes the name and its explicitly versioned collection.
502    ///
503    /// # Type Parameters
504    ///
505    /// * `S` - Serde serializer that determines the output representation.
506    ///
507    /// # Parameters
508    ///
509    /// * `serializer` - Destination receiving the named V1 collection envelope.
510    ///
511    /// # Returns
512    ///
513    /// The serializer's success value after writing the envelope.
514    ///
515    /// # Errors
516    ///
517    /// Returns `S::Error` if wire conversion or serialization fails.
518    #[inline]
519    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
520    where
521        S: Serializer,
522    {
523        let value = ValueWireRefV1::try_from(self.values()).map_err(SerializeError::custom)?;
524        NamedMultiValuesWireRef {
525            name: self.name(),
526            value,
527        }
528        .serialize(serializer)
529    }
530}
531
532impl<'de> Deserialize<'de> for NamedMultiValues {
533    /// Deserializes a named collection from the V1 wire contract.
534    ///
535    /// This implementation validates the wire schema and collection shape, but
536    /// inherits resource accounting from `deserializer`. Callers handling a
537    /// complete, untrusted JSON document should use the bounded JSON helpers on
538    /// [`NamedMultiValues`] instead of an unbounded Serde entry point.
539    /// Generic deserializer errors may contain input-derived text. The bounded
540    /// helpers instead expose only Serde category and location for target
541    /// deserialization failures; resource limits do not sanitize other errors.
542    ///
543    /// # Type Parameters
544    ///
545    /// * `D` - Serde deserializer that supplies the input and any outer budget.
546    ///
547    /// # Parameters
548    ///
549    /// * `deserializer` - Source containing one named V1 collection envelope.
550    ///
551    /// # Returns
552    ///
553    /// The decoded name and homogeneous collection.
554    ///
555    /// # Errors
556    ///
557    /// Returns `D::Error` for an invalid V1 envelope, an unsupported payload,
558    /// or a payload whose shape is not a collection.
559    #[inline]
560    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
561    where
562        D: Deserializer<'de>,
563    {
564        let NamedMultiValuesWireOwned { name, value } =
565            NamedMultiValuesWireOwned::deserialize(deserializer)?;
566        let value = value.into_container().into_collection().map_err(|_| {
567            DeserializeError::custom("named multi-values wire payload must contain a collection")
568        })?;
569        Ok(Self::new(name, value))
570    }
571}