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}