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}