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
14use serde::{
15 Deserialize,
16 Deserializer,
17 Serialize,
18 Serializer,
19};
20
21use crate::ValueWireRefV1;
22#[cfg(feature = "json")]
23use crate::{
24 ValueWireDecodeError,
25 WireLimits,
26};
27
28use super::multi_values::MultiValues;
29use super::named_value::NamedValue;
30
31mod internal;
32
33use internal::{
34 NamedMultiValuesWireOwned,
35 NamedMultiValuesWireRef,
36};
37
38/// Named multiple values
39///
40/// A container that associates a readable name with a set of `MultiValues`,
41/// suitable for organizing data in key-value (name-multiple values) scenarios,
42/// such as configuration items, command-line parameter aggregation, structured
43/// log fields, etc.
44///
45/// # Features
46///
47/// - Provides clear name identification for multiple value collections
48/// - Exposes the inner [`MultiValues`] through explicit accessors
49/// - Supports `serde` serialization and deserialization
50///
51/// # Use Cases
52///
53/// - Aggregating a set of ports, hostnames, etc., as semantically meaningful
54/// fields
55/// - Outputting named multiple value lists in configurations/logs
56///
57/// # Examples
58///
59/// ```rust
60/// use qubit_value::{NamedMultiValues, MultiValues};
61///
62/// // Identify a group of ports with the name "ports"
63/// let named = NamedMultiValues::new(
64/// "ports",
65/// MultiValues::Int32(vec![8080, 8081, 8082])
66/// );
67///
68/// assert_eq!(named.name(), "ports");
69/// assert_eq!(named.values().len(), 3);
70/// ```
71///
72/// The wrapper intentionally does not forward [`MultiValues`] methods
73/// implicitly:
74///
75/// ```compile_fail
76/// use qubit_value::{MultiValues, NamedMultiValues};
77///
78/// let named = NamedMultiValues::new("ports", MultiValues::Int32(vec![8080]));
79/// let _ = named.len();
80/// ```
81#[must_use]
82#[derive(Debug, Clone, PartialEq, Eq, Hash)]
83pub struct NamedMultiValues {
84 /// Name of the values
85 name: String,
86 /// Content of the multiple values
87 value: MultiValues,
88}
89
90impl NamedMultiValues {
91 /// Create a new named multiple values
92 ///
93 /// Associates a given name with `MultiValues`, generating a container that
94 /// can be referenced by name.
95 ///
96 /// # Use Cases
97 ///
98 /// - Building configuration fields (e.g., `servers`, `ports`, etc.)
99 /// - Binding parsed multiple value results to semantic names
100 ///
101 /// # Parameters
102 ///
103 /// * `name` - Name of the multiple values
104 /// * `value` - Content of the multiple values
105 ///
106 /// # Returns
107 ///
108 /// Returns a newly created named multiple values
109 ///
110 /// # Examples
111 ///
112 /// ```rust
113 /// use qubit_value::{NamedMultiValues, MultiValues};
114 ///
115 /// let named = NamedMultiValues::new(
116 /// "servers",
117 /// MultiValues::String(vec!["s1".to_string(), "s2".to_string()])
118 /// );
119 /// assert_eq!(named.name(), "servers");
120 /// ```
121 #[inline]
122 pub fn new(name: impl Into<String>, value: MultiValues) -> Self {
123 Self {
124 name: name.into(),
125 value,
126 }
127 }
128
129 /// Decodes a complete named collection JSON document with default limits.
130 ///
131 /// # Parameters
132 ///
133 /// * `input` - Complete UTF-8 JSON document to decode.
134 ///
135 /// # Returns
136 ///
137 /// The decoded named collection.
138 ///
139 /// # Errors
140 ///
141 /// Returns a JSON, wire-contract, or resource-limit error.
142 #[cfg(feature = "json")]
143 #[inline]
144 pub fn decode_json_slice(
145 input: &[u8],
146 ) -> Result<Self, ValueWireDecodeError> {
147 Self::decode_json_slice_with_limits(input, WireLimits::default())
148 }
149
150 /// Decodes a complete named collection JSON document with explicit limits.
151 ///
152 /// The wrapper name and nested collection share one accounting session.
153 ///
154 /// # Parameters
155 ///
156 /// * `input` - Complete UTF-8 JSON document to decode.
157 /// * `limits` - Input and decoded-resource limits.
158 ///
159 /// # Returns
160 ///
161 /// The decoded named collection.
162 ///
163 /// # Errors
164 ///
165 /// Returns a JSON, wire-contract, or resource-limit error.
166 #[cfg(feature = "json")]
167 pub fn decode_json_slice_with_limits(
168 input: &[u8],
169 limits: WireLimits,
170 ) -> Result<Self, ValueWireDecodeError> {
171 let mut budget = limits.begin(input.len())?;
172 let value: Self = serde_json::from_slice(input)
173 .map_err(ValueWireDecodeError::from)?;
174 budget.check_named_multi_values(&value)?;
175 Ok(value)
176 }
177
178 /// Get a reference to the name
179 ///
180 /// # Returns
181 ///
182 /// Returns a string slice of the name
183 ///
184 /// # Examples
185 ///
186 /// ```rust
187 /// use qubit_value::{NamedMultiValues, MultiValues};
188 ///
189 /// let named = NamedMultiValues::new("items", MultiValues::Int32(vec![1, 2, 3]));
190 /// assert_eq!(named.name(), "items");
191 /// ```
192 #[inline(always)]
193 #[must_use = "the borrowed name should be used"]
194 pub fn name(&self) -> &str {
195 &self.name
196 }
197
198 /// Set a new name
199 ///
200 /// # Parameters
201 ///
202 /// * `name` - The new name
203 ///
204 /// # Returns
205 ///
206 /// No return value
207 ///
208 /// # Examples
209 ///
210 /// ```rust
211 /// use qubit_value::{NamedMultiValues, MultiValues};
212 ///
213 /// let mut named = NamedMultiValues::new("old", MultiValues::Bool(vec![true]));
214 /// named.set_name("new");
215 /// assert_eq!(named.name(), "new");
216 /// ```
217 #[inline(always)]
218 pub fn set_name(&mut self, name: impl Into<String>) {
219 self.name = name.into();
220 }
221
222 /// Borrows the contained values.
223 ///
224 /// # Returns
225 ///
226 /// A shared reference to the contained [`MultiValues`].
227 #[inline(always)]
228 #[must_use = "the borrowed values should be used"]
229 pub fn values(&self) -> &MultiValues {
230 &self.value
231 }
232
233 /// Mutably borrows the contained values.
234 ///
235 /// # Returns
236 ///
237 /// An exclusive reference to the contained [`MultiValues`].
238 #[inline(always)]
239 #[must_use = "the mutable values reference should be used"]
240 pub fn values_mut(&mut self) -> &mut MultiValues {
241 &mut self.value
242 }
243
244 /// Replaces the contained values.
245 ///
246 /// # Parameters
247 ///
248 /// * `values` - New collection to store under the existing name.
249 #[inline(always)]
250 pub fn set_values(&mut self, values: MultiValues) {
251 self.value = values;
252 }
253
254 /// Consumes this wrapper and returns its owned name and values.
255 ///
256 /// # Returns
257 ///
258 /// The `(name, values)` pair without cloning either component.
259 #[inline(always)]
260 #[must_use = "consuming NamedMultiValues without using its parts loses both fields"]
261 pub fn into_parts(self) -> (String, MultiValues) {
262 (self.name, self.value)
263 }
264
265 /// Convert this named multi-values into a named single value.
266 ///
267 /// The returned value keeps the same name and uses the first element from
268 /// the inner [`MultiValues`]. If there is no element, the returned value is
269 /// `Value::Unset` with the same data type.
270 ///
271 /// # Returns
272 ///
273 /// A named clone of the first item, or a named typed unset value.
274 #[inline]
275 pub fn first_named_value(&self) -> NamedValue {
276 NamedValue::new(self.name.as_str(), self.value.first_value())
277 }
278
279 /// Consumes this container and converts its first item to a named value.
280 ///
281 /// The owned name and first stored item are moved into the result. An empty
282 /// or unset collection produces [`crate::Value::Unset`] with the same data
283 /// type.
284 ///
285 /// # Returns
286 ///
287 /// A named owned first item, or a named typed unset value.
288 #[inline]
289 pub fn into_first_named_value(self) -> NamedValue {
290 let (name, values) = self.into_parts();
291 NamedValue::new(name, values.into_first_value())
292 }
293}
294
295impl From<NamedValue> for NamedMultiValues {
296 /// Construct `NamedMultiValues` from `NamedValue`
297 ///
298 /// Reuses the name and promotes the single value to a `MultiValues`
299 /// containing only one element.
300 #[inline]
301 fn from(named: NamedValue) -> Self {
302 let (name, value) = named.into_parts();
303 let value = MultiValues::from(value);
304 Self { name, value }
305 }
306}
307
308impl Serialize for NamedMultiValues {
309 /// Serializes the name and its explicitly versioned collection.
310 #[inline]
311 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
312 where
313 S: Serializer,
314 {
315 let value = ValueWireRefV1::try_from(self.values())
316 .map_err(serde::ser::Error::custom)?;
317 NamedMultiValuesWireRef {
318 name: self.name(),
319 value,
320 }
321 .serialize(serializer)
322 }
323}
324
325impl<'de> Deserialize<'de> for NamedMultiValues {
326 /// Deserializes a named collection from the V1 wire contract.
327 #[inline]
328 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
329 where
330 D: Deserializer<'de>,
331 {
332 let NamedMultiValuesWireOwned { name, value } =
333 NamedMultiValuesWireOwned::deserialize(deserializer)?;
334 let value = value.into_container().into_collection().map_err(|_| {
335 serde::de::Error::custom(
336 "named multi-values wire payload must contain a collection",
337 )
338 })?;
339 Ok(Self::new(name, value))
340 }
341}