qubit_value/named_value.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 Single Value
9//!
10//! Provides a named container for single values, allowing readable identifiers
11//! to be added to individual values in complex configurations or structures.
12//!
13//! Suitable for scenarios such as log annotation, configuration item
14//! encapsulation, and preserving strongly typed values in key-value pairs.
15
16#[cfg(feature = "json")]
17use std::io::Write;
18
19#[cfg(feature = "json")]
20use qubit_budget::json::JsonDecodeLimits;
21#[cfg(feature = "json")]
22use qubit_budget::json::JsonDecodeSession;
23#[cfg(feature = "json")]
24use qubit_budget::json::JsonEncodeLimits;
25#[cfg(feature = "json")]
26use qubit_budget::json::JsonEncodeSession;
27#[cfg(feature = "json")]
28use qubit_json::decode::JsonDecoder;
29#[cfg(feature = "json")]
30use qubit_json::encode::JsonEncoder;
31use serde::Deserialize;
32use serde::Deserializer;
33use serde::Serialize;
34use serde::Serializer;
35use serde::de::Error as DeserializeError;
36use serde::ser::Error as SerializeError;
37
38use self::internal::NamedValueWireOwned;
39use self::internal::NamedValueWireRef;
40use super::value::Value;
41#[cfg(feature = "json")]
42use crate::ValueWireDecodeError;
43#[cfg(feature = "json")]
44use crate::ValueWireEncodeError;
45#[cfg(feature = "json")]
46use crate::ValueWireEncodePreflight;
47use crate::ValueWireRefV1;
48#[cfg(feature = "json")]
49use crate::ValueWireV1;
50
51mod internal;
52
53/// Named single value
54///
55/// Associates a human-readable name with a single [`Value`], facilitating
56/// identification, retrieval, and display in configurations, parameter passing,
57/// and complex data structures.
58///
59/// # Features
60///
61/// - Provides stable name identification for values
62/// - Exposes the inner [`Value`] through explicit accessors
63/// - Supports `serde` serialization and deserialization
64///
65/// # Use Cases
66///
67/// - Configuration item encapsulation (e.g., `"port"`, `"timeout"`, etc.)
68/// - Named output of key values in logs/monitoring
69/// - Quick location by name in collections
70///
71/// # Deserialization boundaries
72///
73/// [`Deserialize`] validates the V1 wire schema and requires a scalar payload,
74/// but it does not create a resource budget by itself. For a complete,
75/// untrusted JSON document, use `NamedValue::decode_json_slice` or
76/// `NamedValue::decode_json_slice_with_limits`. When this type is embedded in a
77/// larger document, deserialize it through a resource-bounded outer decoder.
78/// Bounded helpers report target-deserialization failures as `InvalidJson`
79/// metadata containing only the Serde category and location. Generic Serde
80/// errors retain the surrounding deserializer's semantics and may include
81/// input-derived text, such as field names. Reader and writer I/O error sources
82/// may also contain application-defined details. Resource limits do not
83/// sanitize diagnostics; apply the application's logging policy to all error
84/// sources.
85///
86/// # Examples
87///
88/// ```rust
89/// use qubit_value::{NamedValue, Value};
90///
91/// let named = NamedValue::new("flag", Value::Bool(true));
92/// assert!(named.value().get_bool().unwrap());
93/// ```
94///
95/// The wrapper intentionally does not forward [`Value`] methods implicitly:
96///
97/// ```compile_fail
98/// use qubit_value::{NamedValue, Value};
99///
100/// let named = NamedValue::new("flag", Value::Bool(true));
101/// let _ = named.get_bool();
102/// ```
103#[must_use]
104#[derive(Debug, Clone, PartialEq, Eq, Hash)]
105pub struct NamedValue {
106 /// Owned label, stored independently of the value and its runtime type.
107 name: String,
108 /// Owned scalar payload, retaining its runtime type and unset state.
109 value: Value,
110}
111
112impl NamedValue {
113 /// Create a new named value
114 ///
115 /// Converts the name into owned storage and moves `value` into the wrapper
116 /// without cloning it. The name is not required to be unique or nonempty.
117 ///
118 /// # Type Parameters
119 ///
120 /// * `impl Into<String>` - Name source converted into owned storage.
121 ///
122 /// # Parameters
123 ///
124 /// * `name` - Label converted into an owned string.
125 /// * `value` - Scalar payload whose ownership is transferred to the
126 /// wrapper.
127 ///
128 /// # Returns
129 ///
130 /// Returns a newly created [`NamedValue`] instance
131 ///
132 /// # Examples
133 ///
134 /// ```rust
135 /// use qubit_value::{NamedValue, Value};
136 ///
137 /// let named = NamedValue::new("timeout", Value::Int32(30));
138 /// assert_eq!(named.name(), "timeout");
139 /// ```
140 #[inline(always)]
141 pub fn new(name: impl Into<String>, value: Value) -> Self {
142 Self {
143 name: name.into(),
144 value,
145 }
146 }
147
148 /// Decodes a complete named scalar JSON document with default limits.
149 ///
150 /// # Parameters
151 ///
152 /// * `input` - Complete UTF-8 JSON document to decode.
153 ///
154 /// # Returns
155 ///
156 /// The decoded named scalar.
157 ///
158 /// # Errors
159 ///
160 /// Returns a JSON, wire-contract, or resource-limit error.
161 #[cfg(feature = "json")]
162 #[inline]
163 #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
164 pub fn decode_json_slice(input: &[u8]) -> Result<Self, ValueWireDecodeError> {
165 Self::decode_json_slice_with_limits(input, ValueWireV1::default_json_decode_limits())
166 }
167
168 /// Decodes a complete named scalar JSON document with explicit limits.
169 ///
170 /// The wrapper name and nested scalar share one accounting session.
171 ///
172 /// # Parameters
173 ///
174 /// * `input` - Complete UTF-8 JSON document to decode.
175 /// * `limits` - Input and decoded-resource limits.
176 ///
177 /// # Returns
178 ///
179 /// The decoded named scalar.
180 ///
181 /// # Errors
182 ///
183 /// Returns a JSON, wire-contract, or resource-limit error.
184 #[cfg(feature = "json")]
185 #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
186 pub fn decode_json_slice_with_limits(input: &[u8], limits: JsonDecodeLimits) -> Result<Self, ValueWireDecodeError> {
187 let session = JsonDecodeSession::from_limits(limits);
188 JsonDecoder::new(session)
189 .decode_unique_utf8(input)
190 .map_err(ValueWireDecodeError::from)
191 }
192
193 /// Encodes this named scalar into a bounded compact JSON vector with the
194 /// default V1 JSON resource profile.
195 ///
196 /// # Returns
197 ///
198 /// Compact UTF-8 JSON bytes for the complete named scalar.
199 ///
200 /// # Errors
201 ///
202 /// Returns [`ValueWireEncodeError`] for resource or serialization failures.
203 #[cfg(feature = "json")]
204 #[inline]
205 #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
206 pub fn to_json_vec(&self) -> Result<Vec<u8>, ValueWireEncodeError> {
207 self.to_json_vec_with_limits(ValueWireV1::default_json_encode_limits())
208 }
209
210 /// Encodes this named scalar into a bounded compact JSON vector.
211 ///
212 /// # Parameters
213 ///
214 /// * `limits` - Resource limits enforced during JSON encoding.
215 ///
216 /// # Returns
217 ///
218 /// Compact UTF-8 JSON bytes for the complete named scalar.
219 ///
220 /// # Errors
221 ///
222 /// Returns [`ValueWireEncodeError`] when encoding exceeds `limits` or the
223 /// named scalar cannot be serialized.
224 #[cfg(feature = "json")]
225 #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
226 pub fn to_json_vec_with_limits(&self, limits: JsonEncodeLimits) -> Result<Vec<u8>, ValueWireEncodeError> {
227 ValueWireEncodePreflight::new(limits).check_value(self.value())?;
228 let session = JsonEncodeSession::from_limits(limits);
229 JsonEncoder::new(session)
230 .to_vec(self)
231 .map_err(ValueWireEncodeError::from)
232 }
233
234 /// Encodes this named scalar to a writer with the default V1 JSON profile.
235 ///
236 /// # Type Parameters
237 ///
238 /// * `W` - Destination writer type.
239 ///
240 /// # Parameters
241 ///
242 /// * `writer` - Destination receiving the complete named scalar document.
243 ///
244 /// # Returns
245 ///
246 /// `Ok(())` after the complete document is written.
247 ///
248 /// The complete document is buffered and admitted before this method writes
249 /// its bytes to the destination. Preflight, serialization, or budget
250 /// rejection does not write this document. An I/O error may leave an
251 /// accepted prefix in the writer; do not blindly retry a complete
252 /// document on the same destination.
253 ///
254 /// This method does not explicitly flush the writer. Success means the
255 /// writer accepted all document bytes, not that they were flushed,
256 /// persisted, or received by a remote peer. Pass `&mut writer` to
257 /// retain ownership of a buffered writer, then call `flush()` and
258 /// handle its error separately. Passing an owned writer may invoke that
259 /// writer's drop behavior; errors from an implicit drop-time flush are
260 /// not returned by this method.
261 ///
262 /// # Errors
263 ///
264 /// Returns [`ValueWireEncodeError`] for resource, serialization, or writer
265 /// failures.
266 #[cfg(feature = "json")]
267 #[inline]
268 #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
269 pub fn to_json_writer<W>(&self, writer: W) -> Result<(), ValueWireEncodeError>
270 where
271 W: Write,
272 {
273 self.to_json_writer_with_limits(writer, ValueWireV1::default_json_encode_limits())
274 }
275
276 /// Encodes this named scalar to a writer after enforcing JSON budgets.
277 ///
278 /// # Type Parameters
279 ///
280 /// * `W` - Destination writer type.
281 ///
282 /// # Parameters
283 ///
284 /// * `writer` - Destination receiving the complete named scalar document.
285 /// * `limits` - Resource limits enforced during JSON encoding.
286 ///
287 /// # Returns
288 ///
289 /// `Ok(())` after the complete document is written.
290 ///
291 /// The complete document is buffered and admitted before this method writes
292 /// its bytes to the destination. Preflight, serialization, or budget
293 /// rejection does not write this document. An I/O error may leave an
294 /// accepted prefix in the writer; do not blindly retry a complete
295 /// document on the same destination.
296 ///
297 /// This method does not explicitly flush the writer. Success means the
298 /// writer accepted all document bytes, not that they were flushed,
299 /// persisted, or received by a remote peer. Pass `&mut writer` to
300 /// retain ownership of a buffered writer, then call `flush()` and
301 /// handle its error separately. Passing an owned writer may invoke that
302 /// writer's drop behavior; errors from an implicit drop-time flush are
303 /// not returned by this method.
304 ///
305 /// # Errors
306 ///
307 /// Returns [`ValueWireEncodeError`] when encoding exceeds `limits`, the
308 /// named scalar cannot be serialized, or `writer` rejects output.
309 #[cfg(feature = "json")]
310 #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
311 pub fn to_json_writer_with_limits<W>(
312 &self,
313 mut writer: W,
314 limits: JsonEncodeLimits,
315 ) -> Result<(), ValueWireEncodeError>
316 where
317 W: Write,
318 {
319 ValueWireEncodePreflight::new(limits).check_value(self.value())?;
320 let session = JsonEncodeSession::from_limits(limits);
321 JsonEncoder::new(session)
322 .write_buffered(&mut writer, self)
323 .map_err(ValueWireEncodeError::from)
324 }
325
326 /// Get a reference to the name
327 ///
328 /// Borrows the stored label without allocating. The slice remains valid
329 /// while this wrapper is shared-borrowed, preventing name replacement
330 /// through the wrapper during that borrow.
331 ///
332 /// # Returns
333 ///
334 /// Returns a string slice `&str` of the name
335 ///
336 /// # Examples
337 ///
338 /// ```rust
339 /// use qubit_value::{NamedValue, Value};
340 ///
341 /// let named = NamedValue::new("host", Value::String("localhost".to_string()));
342 /// assert_eq!(named.name(), "host");
343 /// ```
344 #[inline(always)]
345 #[must_use = "the borrowed name should be used"]
346 pub fn name(&self) -> &str {
347 &self.name
348 }
349
350 /// Borrows the contained value without cloning its payload.
351 ///
352 /// The reference is tied to the shared borrow of this wrapper; mutation
353 /// through the wrapper requires that borrow to end.
354 ///
355 /// # Returns
356 ///
357 /// A shared reference to the contained [`Value`].
358 #[inline(always)]
359 #[must_use = "the borrowed value should be used"]
360 pub fn value(&self) -> &Value {
361 &self.value
362 }
363
364 /// Borrows the contained value exclusively, allowing in-place updates.
365 ///
366 /// The reference is tied to the exclusive borrow of this wrapper. Updates
367 /// can change the payload or its runtime type without changing the name.
368 ///
369 /// # Returns
370 ///
371 /// An exclusive reference to the contained [`Value`].
372 #[inline]
373 #[must_use = "the mutable value reference should be used"]
374 pub fn value_mut(&mut self) -> &mut Value {
375 &mut self.value
376 }
377
378 /// Set a new name
379 ///
380 /// Converts the new label into owned storage and drops the previous name,
381 /// leaving the contained value unchanged. Empty and duplicate labels are
382 /// accepted without validation.
383 ///
384 /// # Type Parameters
385 ///
386 /// * `impl Into<String>` - Name source converted into owned storage.
387 ///
388 /// # Parameters
389 ///
390 /// * `name` - The new name
391 ///
392 /// # Examples
393 ///
394 /// ```rust
395 /// use qubit_value::{NamedValue, Value};
396 ///
397 /// let mut named = NamedValue::new("old_name", Value::Bool(true));
398 /// named.set_name("new_name");
399 /// assert_eq!(named.name(), "new_name");
400 /// ```
401 #[inline(always)]
402 pub fn set_name(&mut self, name: impl Into<String>) {
403 self.name = name.into();
404 }
405
406 /// Moves a new value into this wrapper and drops the previous payload.
407 ///
408 /// The replacement may change the runtime type or unset state; the stored
409 /// name is preserved and the new payload is not cloned.
410 ///
411 /// # Parameters
412 ///
413 /// * `value` - New value to store under the existing name.
414 #[inline]
415 pub fn set_value(&mut self, value: Value) {
416 self.value = value;
417 }
418
419 /// Consumes this wrapper and returns its owned name and value.
420 ///
421 /// # Returns
422 ///
423 /// The `(name, value)` pair without cloning either component.
424 #[inline]
425 #[must_use = "consuming NamedValue without using its parts loses both fields"]
426 pub fn into_parts(self) -> (String, Value) {
427 (self.name, self.value)
428 }
429}
430
431impl Serialize for NamedValue {
432 /// Serializes the name and its explicitly versioned scalar value.
433 ///
434 /// # Type Parameters
435 ///
436 /// * `S` - Serde serializer that determines the output format.
437 ///
438 /// # Parameters
439 ///
440 /// * `serializer` - Destination serializer receiving the named V1 scalar
441 /// envelope.
442 ///
443 /// # Returns
444 ///
445 /// The serializer's success value after serializing the complete envelope.
446 ///
447 /// # Errors
448 ///
449 /// Returns `S::Error` when the scalar cannot be represented by the V1 wire
450 /// contract or the serializer rejects the envelope.
451 #[inline]
452 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
453 where
454 S: Serializer,
455 {
456 let value = ValueWireRefV1::try_from(self.value()).map_err(SerializeError::custom)?;
457 NamedValueWireRef {
458 name: self.name(),
459 value,
460 }
461 .serialize(serializer)
462 }
463}
464
465impl<'de> Deserialize<'de> for NamedValue {
466 /// Deserializes a named scalar value from the V1 wire contract.
467 ///
468 /// This implementation validates the wire schema and scalar shape, but
469 /// inherits resource accounting from `deserializer`. Callers handling a
470 /// complete, untrusted JSON document should use the bounded JSON helpers on
471 /// [`NamedValue`] instead of an unbounded Serde entry point.
472 /// Generic deserializer errors may contain input-derived text. The bounded
473 /// helpers instead expose only Serde category and location for target
474 /// deserialization failures; resource limits do not sanitize other errors.
475 ///
476 /// # Type Parameters
477 ///
478 /// * `D` - Serde deserializer that supplies the input and any outer budget.
479 ///
480 /// # Parameters
481 ///
482 /// * `deserializer` - Source containing one named V1 scalar envelope.
483 ///
484 /// # Returns
485 ///
486 /// The decoded name and scalar value.
487 ///
488 /// # Errors
489 ///
490 /// Returns `D::Error` for an invalid V1 envelope, an unsupported payload,
491 /// or a payload whose shape is not scalar.
492 #[inline]
493 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
494 where
495 D: Deserializer<'de>,
496 {
497 let NamedValueWireOwned { name, value } = NamedValueWireOwned::deserialize(deserializer)?;
498 let value = value
499 .into_container()
500 .into_scalar()
501 .map_err(|_| DeserializeError::custom("named value wire payload must contain a scalar"))?;
502 Ok(Self::new(name, value))
503 }
504}