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 super::value::Value;
39#[cfg(feature = "json")]
40use crate::ValueWireDecodeError;
41#[cfg(feature = "json")]
42use crate::ValueWireEncodeError;
43use crate::ValueWireRefV1;
44#[cfg(feature = "json")]
45use crate::ValueWireV1;
46
47mod internal;
48
49use self::internal::NamedValueWireOwned;
50use self::internal::NamedValueWireRef;
51
52/// Named single value
53///
54/// Associates a human-readable name with a single [`Value`], facilitating
55/// identification, retrieval, and display in configurations, parameter passing,
56/// and complex data structures.
57///
58/// # Features
59///
60/// - Provides stable name identification for values
61/// - Exposes the inner [`Value`] through explicit accessors
62/// - Supports `serde` serialization and deserialization
63///
64/// # Use Cases
65///
66/// - Configuration item encapsulation (e.g., `"port"`, `"timeout"`, etc.)
67/// - Named output of key values in logs/monitoring
68/// - Quick location by name in collections
69///
70/// # Deserialization boundaries
71///
72/// [`Deserialize`] validates the V1 wire schema and requires a scalar payload,
73/// but it does not create a resource budget by itself. For a complete,
74/// untrusted JSON document, use `NamedValue::decode_json_slice` or
75/// `NamedValue::decode_json_slice_with_limits`. When this type is embedded in a
76/// larger document, deserialize it through a resource-bounded outer decoder.
77///
78/// # Examples
79///
80/// ```rust
81/// use qubit_value::{NamedValue, Value};
82///
83/// let named = NamedValue::new("flag", Value::Bool(true));
84/// assert!(named.value().get_bool().unwrap());
85/// ```
86///
87/// The wrapper intentionally does not forward [`Value`] methods implicitly:
88///
89/// ```compile_fail
90/// use qubit_value::{NamedValue, Value};
91///
92/// let named = NamedValue::new("flag", Value::Bool(true));
93/// let _ = named.get_bool();
94/// ```
95#[must_use]
96#[derive(Debug, Clone, PartialEq, Eq, Hash)]
97pub struct NamedValue {
98 /// Name of the value
99 name: String,
100 /// Content of the value
101 value: Value,
102}
103
104impl NamedValue {
105 /// Create a new named value
106 ///
107 /// Creates a binding instance between a name and a value.
108 ///
109 /// # Type Parameters
110 ///
111 /// * `impl Into<String>` - Name source converted into owned storage.
112 ///
113 /// # Parameters
114 ///
115 /// * `name` - Name of the value
116 /// * `value` - Content of the value
117 ///
118 /// # Returns
119 ///
120 /// Returns a newly created [`NamedValue`] instance
121 ///
122 /// # Examples
123 ///
124 /// ```rust
125 /// use qubit_value::{NamedValue, Value};
126 ///
127 /// let named = NamedValue::new("timeout", Value::Int32(30));
128 /// assert_eq!(named.name(), "timeout");
129 /// ```
130 #[inline(always)]
131 pub fn new(name: impl Into<String>, value: Value) -> Self {
132 Self {
133 name: name.into(),
134 value,
135 }
136 }
137
138 /// Decodes a complete named scalar JSON document with default limits.
139 ///
140 /// # Parameters
141 ///
142 /// * `input` - Complete UTF-8 JSON document to decode.
143 ///
144 /// # Returns
145 ///
146 /// The decoded named scalar.
147 ///
148 /// # Errors
149 ///
150 /// Returns a JSON, wire-contract, or resource-limit error.
151 #[cfg(feature = "json")]
152 #[inline(always)]
153 pub fn decode_json_slice(input: &[u8]) -> Result<Self, ValueWireDecodeError> {
154 Self::decode_json_slice_with_limits(input, ValueWireV1::default_json_decode_limits())
155 }
156
157 /// Decodes a complete named scalar JSON document with explicit limits.
158 ///
159 /// The wrapper name and nested scalar share one accounting session.
160 ///
161 /// # Parameters
162 ///
163 /// * `input` - Complete UTF-8 JSON document to decode.
164 /// * `limits` - Input and decoded-resource limits.
165 ///
166 /// # Returns
167 ///
168 /// The decoded named scalar.
169 ///
170 /// # Errors
171 ///
172 /// Returns a JSON, wire-contract, or resource-limit error.
173 #[cfg(feature = "json")]
174 pub fn decode_json_slice_with_limits(input: &[u8], limits: JsonDecodeLimits) -> Result<Self, ValueWireDecodeError> {
175 let session = JsonDecodeSession::from_limits(limits);
176 JsonDecoder::new(session)
177 .decode_utf8(input)
178 .map_err(ValueWireDecodeError::from)
179 }
180
181 /// Encodes this named scalar into a bounded compact JSON vector with the
182 /// default V1 JSON resource profile.
183 ///
184 /// # Returns
185 ///
186 /// Compact UTF-8 JSON bytes for the complete named scalar.
187 ///
188 /// # Errors
189 ///
190 /// Returns [`ValueWireEncodeError`] for resource or serialization failures.
191 #[cfg(feature = "json")]
192 #[inline(always)]
193 pub fn to_json_vec(&self) -> Result<Vec<u8>, ValueWireEncodeError> {
194 self.to_json_vec_with_limits(ValueWireV1::default_json_encode_limits())
195 }
196
197 /// Encodes this named scalar into a bounded compact JSON vector.
198 ///
199 /// # Parameters
200 ///
201 /// * `limits` - Resource limits enforced during JSON encoding.
202 ///
203 /// # Returns
204 ///
205 /// Compact UTF-8 JSON bytes for the complete named scalar.
206 ///
207 /// # Errors
208 ///
209 /// Returns [`ValueWireEncodeError`] when encoding exceeds `limits` or the
210 /// named scalar cannot be serialized.
211 #[cfg(feature = "json")]
212 pub fn to_json_vec_with_limits(&self, limits: JsonEncodeLimits) -> Result<Vec<u8>, ValueWireEncodeError> {
213 let session = JsonEncodeSession::from_limits(limits);
214 JsonEncoder::new(session)
215 .to_vec(self)
216 .map_err(ValueWireEncodeError::from)
217 }
218
219 /// Encodes this named scalar to a writer with the default V1 JSON profile.
220 ///
221 /// # Type Parameters
222 ///
223 /// * `W` - Destination writer type.
224 ///
225 /// # Parameters
226 ///
227 /// * `writer` - Destination receiving the complete named scalar document.
228 ///
229 /// # Returns
230 ///
231 /// `Ok(())` after the complete document is written.
232 ///
233 /// # Errors
234 ///
235 /// Returns [`ValueWireEncodeError`] for resource, serialization, or writer
236 /// failures.
237 #[cfg(feature = "json")]
238 #[inline(always)]
239 pub fn to_json_writer<W>(&self, writer: W) -> Result<(), ValueWireEncodeError>
240 where
241 W: Write,
242 {
243 self.to_json_writer_with_limits(writer, ValueWireV1::default_json_encode_limits())
244 }
245
246 /// Encodes this named scalar to a writer after enforcing JSON budgets.
247 ///
248 /// # Type Parameters
249 ///
250 /// * `W` - Destination writer type.
251 ///
252 /// # Parameters
253 ///
254 /// * `writer` - Destination receiving the complete named scalar document.
255 /// * `limits` - Resource limits enforced during JSON encoding.
256 ///
257 /// # Returns
258 ///
259 /// `Ok(())` after the complete document is written.
260 ///
261 /// # Errors
262 ///
263 /// Returns [`ValueWireEncodeError`] when encoding exceeds `limits`, the
264 /// named scalar cannot be serialized, or `writer` rejects output.
265 #[cfg(feature = "json")]
266 pub fn to_json_writer_with_limits<W>(&self, writer: W, limits: JsonEncodeLimits) -> Result<(), ValueWireEncodeError>
267 where
268 W: Write,
269 {
270 let session = JsonEncodeSession::from_limits(limits);
271 JsonEncoder::new(session)
272 .write_buffered(writer, self)
273 .map_err(ValueWireEncodeError::from)
274 }
275
276 /// Get a reference to the name
277 ///
278 /// Returns a read-only name slice bound to this value.
279 ///
280 /// # Returns
281 ///
282 /// Returns a string slice `&str` of the name
283 ///
284 /// # Examples
285 ///
286 /// ```rust
287 /// use qubit_value::{NamedValue, Value};
288 ///
289 /// let named = NamedValue::new("host", Value::String("localhost".to_string()));
290 /// assert_eq!(named.name(), "host");
291 /// ```
292 #[inline(always)]
293 #[must_use = "the borrowed name should be used"]
294 pub fn name(&self) -> &str {
295 &self.name
296 }
297
298 /// Set a new name
299 ///
300 /// Updates the name bound to the current instance.
301 ///
302 /// # Type Parameters
303 ///
304 /// * `impl Into<String>` - Name source converted into owned storage.
305 ///
306 /// # Parameters
307 ///
308 /// * `name` - The new name
309 ///
310 /// # Examples
311 ///
312 /// ```rust
313 /// use qubit_value::{NamedValue, Value};
314 ///
315 /// let mut named = NamedValue::new("old_name", Value::Bool(true));
316 /// named.set_name("new_name");
317 /// assert_eq!(named.name(), "new_name");
318 /// ```
319 #[inline(always)]
320 pub fn set_name(&mut self, name: impl Into<String>) {
321 self.name = name.into();
322 }
323
324 /// Borrows the contained value.
325 ///
326 /// # Returns
327 ///
328 /// A shared reference to the contained [`Value`].
329 #[inline(always)]
330 #[must_use = "the borrowed value should be used"]
331 pub fn value(&self) -> &Value {
332 &self.value
333 }
334
335 /// Mutably borrows the contained value.
336 ///
337 /// # Returns
338 ///
339 /// An exclusive reference to the contained [`Value`].
340 #[inline(always)]
341 #[must_use = "the mutable value reference should be used"]
342 pub fn value_mut(&mut self) -> &mut Value {
343 &mut self.value
344 }
345
346 /// Replaces the contained value.
347 ///
348 /// # Parameters
349 ///
350 /// * `value` - New value to store under the existing name.
351 #[inline(always)]
352 pub fn set_value(&mut self, value: Value) {
353 self.value = value;
354 }
355
356 /// Consumes this wrapper and returns its owned name and value.
357 ///
358 /// # Returns
359 ///
360 /// The `(name, value)` pair without cloning either component.
361 #[inline(always)]
362 #[must_use = "consuming NamedValue without using its parts loses both fields"]
363 pub fn into_parts(self) -> (String, Value) {
364 (self.name, self.value)
365 }
366}
367
368impl Serialize for NamedValue {
369 /// Serializes the name and its explicitly versioned scalar value.
370 #[inline]
371 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
372 where
373 S: Serializer,
374 {
375 let value = ValueWireRefV1::try_from(self.value()).map_err(SerializeError::custom)?;
376 NamedValueWireRef {
377 name: self.name(),
378 value,
379 }
380 .serialize(serializer)
381 }
382}
383
384impl<'de> Deserialize<'de> for NamedValue {
385 /// Deserializes a named scalar value from the V1 wire contract.
386 ///
387 /// This implementation validates the wire schema and scalar shape, but
388 /// inherits resource accounting from `deserializer`. Callers handling a
389 /// complete, untrusted JSON document should use the bounded JSON helpers on
390 /// [`NamedValue`] instead of an unbounded Serde entry point.
391 ///
392 /// # Type Parameters
393 ///
394 /// * `D` - Serde deserializer that supplies the input and any outer budget.
395 ///
396 /// # Parameters
397 ///
398 /// * `deserializer` - Source containing one named V1 scalar envelope.
399 ///
400 /// # Returns
401 ///
402 /// The decoded name and scalar value.
403 ///
404 /// # Errors
405 ///
406 /// Returns `D::Error` for an invalid V1 envelope, an unsupported payload,
407 /// or a payload whose shape is not scalar.
408 #[inline]
409 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
410 where
411 D: Deserializer<'de>,
412 {
413 let NamedValueWireOwned { name, value } = NamedValueWireOwned::deserialize(deserializer)?;
414 let value = value
415 .into_container()
416 .into_scalar()
417 .map_err(|_| DeserializeError::custom("named value wire payload must contain a scalar"))?;
418 Ok(Self::new(name, value))
419 }
420}