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}