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 super::multi_values::MultiValues;
37use super::named_value::NamedValue;
38#[cfg(feature = "json")]
39use crate::ValueWireDecodeError;
40#[cfg(feature = "json")]
41use crate::ValueWireEncodeError;
42use crate::ValueWireRefV1;
43#[cfg(feature = "json")]
44use crate::ValueWireV1;
45
46mod internal;
47
48use self::internal::NamedMultiValuesWireOwned;
49use self::internal::NamedMultiValuesWireRef;
50
51/// Named multiple values
52///
53/// A container that associates a readable name with a set of `MultiValues`,
54/// suitable for organizing data in key-value (name-multiple values) scenarios,
55/// such as configuration items, command-line parameter aggregation, structured
56/// log fields, etc.
57///
58/// # Features
59///
60/// - Provides clear name identification for multiple value collections
61/// - Exposes the inner [`MultiValues`] through explicit accessors
62/// - Supports `serde` serialization and deserialization
63///
64/// # Use Cases
65///
66/// - Aggregating a set of ports, hostnames, etc., as semantically meaningful
67///   fields
68/// - Outputting named multiple value lists in configurations/logs
69///
70/// # Deserialization boundaries
71///
72/// [`Deserialize`] validates the V1 wire schema and requires a collection
73/// payload, but it does not create a resource budget by itself. For a complete,
74/// untrusted JSON document, use `NamedMultiValues::decode_json_slice` or
75/// `NamedMultiValues::decode_json_slice_with_limits`. When this type is
76/// embedded in a larger document, deserialize it through a resource-bounded
77/// outer decoder.
78///
79/// # Examples
80///
81/// ```rust
82/// use qubit_value::{NamedMultiValues, MultiValues};
83///
84/// // Identify a group of ports with the name "ports"
85/// let named = NamedMultiValues::new(
86///     "ports",
87///     MultiValues::Int32(vec![8080, 8081, 8082])
88/// );
89///
90/// assert_eq!(named.name(), "ports");
91/// assert_eq!(named.values().len(), 3);
92/// ```
93///
94/// The wrapper intentionally does not forward [`MultiValues`] methods
95/// implicitly:
96///
97/// ```compile_fail
98/// use qubit_value::{MultiValues, NamedMultiValues};
99///
100/// let named = NamedMultiValues::new("ports", MultiValues::Int32(vec![8080]));
101/// let _ = named.len();
102/// ```
103#[must_use]
104#[derive(Debug, Clone, PartialEq, Eq, Hash)]
105pub struct NamedMultiValues {
106    /// Name of the values
107    name: String,
108    /// Content of the multiple values
109    value: MultiValues,
110}
111
112impl NamedMultiValues {
113    /// Create a new named multiple values
114    ///
115    /// Associates a given name with `MultiValues`, generating a container that
116    /// can be referenced by name.
117    ///
118    /// # Type Parameters
119    ///
120    /// * `impl Into<String>` - Name source converted into owned storage.
121    ///
122    /// # Use Cases
123    ///
124    /// - Building configuration fields (e.g., `servers`, `ports`, etc.)
125    /// - Binding parsed multiple value results to semantic names
126    ///
127    /// # Parameters
128    ///
129    /// * `name` - Name of the multiple values
130    /// * `value` - Content of the multiple values
131    ///
132    /// # Returns
133    ///
134    /// Returns a newly created named multiple values
135    ///
136    /// # Examples
137    ///
138    /// ```rust
139    /// use qubit_value::{NamedMultiValues, MultiValues};
140    ///
141    /// let named = NamedMultiValues::new(
142    ///     "servers",
143    ///     MultiValues::String(vec!["s1".to_string(), "s2".to_string()])
144    /// );
145    /// assert_eq!(named.name(), "servers");
146    /// ```
147    #[inline(always)]
148    pub fn new(name: impl Into<String>, value: MultiValues) -> Self {
149        Self {
150            name: name.into(),
151            value,
152        }
153    }
154
155    /// Decodes a complete named collection JSON document with default limits.
156    ///
157    /// # Parameters
158    ///
159    /// * `input` - Complete UTF-8 JSON document to decode.
160    ///
161    /// # Returns
162    ///
163    /// The decoded named collection.
164    ///
165    /// # Errors
166    ///
167    /// Returns a JSON, wire-contract, or resource-limit error.
168    #[cfg(feature = "json")]
169    #[inline(always)]
170    pub fn decode_json_slice(input: &[u8]) -> Result<Self, ValueWireDecodeError> {
171        Self::decode_json_slice_with_limits(input, ValueWireV1::default_json_decode_limits())
172    }
173
174    /// Decodes a complete named collection JSON document with explicit limits.
175    ///
176    /// The wrapper name and nested collection share one accounting session.
177    ///
178    /// # Parameters
179    ///
180    /// * `input` - Complete UTF-8 JSON document to decode.
181    /// * `limits` - Input and decoded-resource limits.
182    ///
183    /// # Returns
184    ///
185    /// The decoded named collection.
186    ///
187    /// # Errors
188    ///
189    /// Returns a JSON, wire-contract, or resource-limit error.
190    #[cfg(feature = "json")]
191    pub fn decode_json_slice_with_limits(input: &[u8], limits: JsonDecodeLimits) -> Result<Self, ValueWireDecodeError> {
192        let session = JsonDecodeSession::from_limits(limits);
193        JsonDecoder::new(session)
194            .decode_utf8(input)
195            .map_err(ValueWireDecodeError::from)
196    }
197
198    /// Encodes this named collection into a bounded compact JSON vector with
199    /// the default V1 JSON resource profile.
200    ///
201    /// # Returns
202    ///
203    /// Compact UTF-8 JSON bytes for the complete named collection.
204    ///
205    /// # Errors
206    ///
207    /// Returns [`ValueWireEncodeError`] for resource or serialization failures.
208    #[cfg(feature = "json")]
209    #[inline(always)]
210    pub fn to_json_vec(&self) -> Result<Vec<u8>, ValueWireEncodeError> {
211        self.to_json_vec_with_limits(ValueWireV1::default_json_encode_limits())
212    }
213
214    /// Encodes this named collection into a bounded compact JSON vector.
215    ///
216    /// # Parameters
217    ///
218    /// * `limits` - Resource limits enforced during JSON encoding.
219    ///
220    /// # Returns
221    ///
222    /// Compact UTF-8 JSON bytes for the complete named collection.
223    ///
224    /// # Errors
225    ///
226    /// Returns [`ValueWireEncodeError`] when encoding exceeds `limits` or the
227    /// named collection cannot be serialized.
228    #[cfg(feature = "json")]
229    pub fn to_json_vec_with_limits(&self, limits: JsonEncodeLimits) -> Result<Vec<u8>, ValueWireEncodeError> {
230        let session = JsonEncodeSession::from_limits(limits);
231        JsonEncoder::new(session)
232            .to_vec(self)
233            .map_err(ValueWireEncodeError::from)
234    }
235
236    /// Encodes this named collection to a writer with the default V1 JSON
237    /// profile.
238    ///
239    /// # Type Parameters
240    ///
241    /// * `W` - Destination writer type.
242    ///
243    /// # Parameters
244    ///
245    /// * `writer` - Destination receiving the complete named collection.
246    ///
247    /// # Returns
248    ///
249    /// `Ok(())` after the complete document is written.
250    ///
251    /// # Errors
252    ///
253    /// Returns [`ValueWireEncodeError`] for resource, serialization, or writer
254    /// failures.
255    #[cfg(feature = "json")]
256    #[inline(always)]
257    pub fn to_json_writer<W>(&self, writer: W) -> Result<(), ValueWireEncodeError>
258    where
259        W: Write,
260    {
261        self.to_json_writer_with_limits(writer, ValueWireV1::default_json_encode_limits())
262    }
263
264    /// Encodes this named collection to a writer after enforcing JSON budgets.
265    ///
266    /// # Type Parameters
267    ///
268    /// * `W` - Destination writer type.
269    ///
270    /// # Parameters
271    ///
272    /// * `writer` - Destination receiving the complete named collection.
273    /// * `limits` - Resource limits enforced during JSON encoding.
274    ///
275    /// # Returns
276    ///
277    /// `Ok(())` after the complete document is written.
278    ///
279    /// # Errors
280    ///
281    /// Returns [`ValueWireEncodeError`] when encoding exceeds `limits`, the
282    /// named collection cannot be serialized, or `writer` rejects output.
283    #[cfg(feature = "json")]
284    pub fn to_json_writer_with_limits<W>(&self, writer: W, limits: JsonEncodeLimits) -> Result<(), ValueWireEncodeError>
285    where
286        W: Write,
287    {
288        let session = JsonEncodeSession::from_limits(limits);
289        JsonEncoder::new(session)
290            .write_buffered(writer, self)
291            .map_err(ValueWireEncodeError::from)
292    }
293
294    /// Get a reference to the name
295    ///
296    /// # Returns
297    ///
298    /// Returns a string slice of the name
299    ///
300    /// # Examples
301    ///
302    /// ```rust
303    /// use qubit_value::{NamedMultiValues, MultiValues};
304    ///
305    /// let named = NamedMultiValues::new("items", MultiValues::Int32(vec![1, 2, 3]));
306    /// assert_eq!(named.name(), "items");
307    /// ```
308    #[inline(always)]
309    #[must_use = "the borrowed name should be used"]
310    pub fn name(&self) -> &str {
311        &self.name
312    }
313
314    /// Set a new name
315    ///
316    /// # Type Parameters
317    ///
318    /// * `impl Into<String>` - Name source converted into owned storage.
319    ///
320    /// # Parameters
321    ///
322    /// * `name` - The new name
323    ///
324    /// # Returns
325    ///
326    /// No return value
327    ///
328    /// # Examples
329    ///
330    /// ```rust
331    /// use qubit_value::{NamedMultiValues, MultiValues};
332    ///
333    /// let mut named = NamedMultiValues::new("old", MultiValues::Bool(vec![true]));
334    /// named.set_name("new");
335    /// assert_eq!(named.name(), "new");
336    /// ```
337    #[inline(always)]
338    pub fn set_name(&mut self, name: impl Into<String>) {
339        self.name = name.into();
340    }
341
342    /// Borrows the contained values.
343    ///
344    /// # Returns
345    ///
346    /// A shared reference to the contained [`MultiValues`].
347    #[inline(always)]
348    #[must_use = "the borrowed values should be used"]
349    pub fn values(&self) -> &MultiValues {
350        &self.value
351    }
352
353    /// Mutably borrows the contained values.
354    ///
355    /// # Returns
356    ///
357    /// An exclusive reference to the contained [`MultiValues`].
358    #[inline(always)]
359    #[must_use = "the mutable values reference should be used"]
360    pub fn values_mut(&mut self) -> &mut MultiValues {
361        &mut self.value
362    }
363
364    /// Replaces the contained values.
365    ///
366    /// # Parameters
367    ///
368    /// * `values` - New collection to store under the existing name.
369    #[inline(always)]
370    pub fn set_values(&mut self, values: MultiValues) {
371        self.value = values;
372    }
373
374    /// Consumes this wrapper and returns its owned name and values.
375    ///
376    /// # Returns
377    ///
378    /// The `(name, values)` pair without cloning either component.
379    #[inline(always)]
380    #[must_use = "consuming NamedMultiValues without using its parts loses both fields"]
381    pub fn into_parts(self) -> (String, MultiValues) {
382        (self.name, self.value)
383    }
384
385    /// Convert this named multi-values into a named single value.
386    ///
387    /// The returned value keeps the same name and uses the first element from
388    /// the inner [`MultiValues`]. If there is no element, the returned value is
389    /// `Value::Unset` with the same data type.
390    ///
391    /// # Returns
392    ///
393    /// A named clone of the first item, or a named typed unset value.
394    #[must_use = "the projected named value should be used"]
395    #[inline(always)]
396    pub fn first_named_value(&self) -> NamedValue {
397        NamedValue::new(self.name.as_str(), self.value.first_value())
398    }
399
400    /// Consumes this container and converts its first item to a named value.
401    ///
402    /// The owned name and first stored item are moved into the result. An empty
403    /// or unset collection produces [`crate::Value::Unset`] with the same data
404    /// type.
405    ///
406    /// # Returns
407    ///
408    /// A named owned first item, or a named typed unset value.
409    #[inline]
410    pub fn into_first_named_value(self) -> NamedValue {
411        let (name, values) = self.into_parts();
412        NamedValue::new(name, values.into_first_value())
413    }
414}
415
416impl From<NamedValue> for NamedMultiValues {
417    /// Construct `NamedMultiValues` from `NamedValue`
418    ///
419    /// Reuses the name and promotes the single value to a `MultiValues`
420    /// containing only one element.
421    #[inline]
422    fn from(named: NamedValue) -> Self {
423        let (name, value) = named.into_parts();
424        let value = MultiValues::from(value);
425        Self { name, value }
426    }
427}
428
429impl Serialize for NamedMultiValues {
430    /// Serializes the name and its explicitly versioned collection.
431    #[inline]
432    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
433    where
434        S: Serializer,
435    {
436        let value = ValueWireRefV1::try_from(self.values()).map_err(SerializeError::custom)?;
437        NamedMultiValuesWireRef {
438            name: self.name(),
439            value,
440        }
441        .serialize(serializer)
442    }
443}
444
445impl<'de> Deserialize<'de> for NamedMultiValues {
446    /// Deserializes a named collection from the V1 wire contract.
447    ///
448    /// This implementation validates the wire schema and collection shape, but
449    /// inherits resource accounting from `deserializer`. Callers handling a
450    /// complete, untrusted JSON document should use the bounded JSON helpers on
451    /// [`NamedMultiValues`] instead of an unbounded Serde entry point.
452    ///
453    /// # Type Parameters
454    ///
455    /// * `D` - Serde deserializer that supplies the input and any outer budget.
456    ///
457    /// # Parameters
458    ///
459    /// * `deserializer` - Source containing one named V1 collection envelope.
460    ///
461    /// # Returns
462    ///
463    /// The decoded name and homogeneous collection.
464    ///
465    /// # Errors
466    ///
467    /// Returns `D::Error` for an invalid V1 envelope, an unsupported payload,
468    /// or a payload whose shape is not a collection.
469    #[inline]
470    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
471    where
472        D: Deserializer<'de>,
473    {
474        let NamedMultiValuesWireOwned { name, value } = NamedMultiValuesWireOwned::deserialize(deserializer)?;
475        let value = value
476            .into_container()
477            .into_collection()
478            .map_err(|_| DeserializeError::custom("named multi-values wire payload must contain a collection"))?;
479        Ok(Self::new(name, value))
480    }
481}