Skip to main content

qubit_value/
value_missing.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//! Missing-value facts retained across storage and conversion boundaries.
9
10use std::fmt;
11
12#[cfg(feature = "converter")]
13use qubit_datatype::DataConversionError;
14#[cfg(feature = "converter")]
15use qubit_datatype::DataConversionErrorKind;
16use qubit_datatype::DataType;
17
18use crate::ValueMissingReason;
19
20/// Describes the storage state, requested type and source of a missing read.
21///
22/// The fields are private so callers cannot manufacture an incomplete fact.
23/// Use the constructors for storage states and the accessors for the optional
24/// facts. `source_type` and `target_type` are `None` only when the caller has
25/// no type information (for example, a generic empty iterator); `source_index`
26/// is set only when a collection item caused the failure. A preserved
27/// `conversion_error` is available only when the `converter` feature is on.
28///
29/// Strict fallback is allowed only for an unset value after type admission.
30/// Conversion fallback additionally accepts a policy-classified missing scalar
31/// but never a concrete empty collection or a missing collection item.
32///
33/// # Examples
34///
35/// ```
36/// use qubit_datatype::DataType;
37/// use qubit_value::{Value, ValueMissingReason};
38///
39/// let error = Value::new_unset(DataType::Int32).get::<i32>().unwrap_err();
40/// let missing = error.missing().unwrap();
41/// assert_eq!(missing.reason(), ValueMissingReason::UnsetScalar);
42/// assert_eq!(missing.source_type(), Some(DataType::Int32));
43/// assert_eq!(missing.target_type(), Some(DataType::Int32));
44/// assert!(missing.is_defaultable_for_strict_read());
45/// ```
46///
47/// Strict reads record both source and target. Conversion failures additionally
48/// retain their original error and, for collection items, source index.
49/// Inspect [`Self::reason`] and the accessors instead of matching storage
50/// fields.
51#[must_use]
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct ValueMissing {
54    reason: ValueMissingReason,
55    source_type: Option<DataType>,
56    target_type: Option<DataType>,
57    source_index: Option<usize>,
58    #[cfg(feature = "converter")]
59    conversion_error: Option<DataConversionError>,
60}
61
62impl ValueMissing {
63    /// Creates an unset scalar descriptor for a read from `source` to `target`.
64    ///
65    /// # Parameters
66    ///
67    /// * `source` - Runtime type of the unset scalar storage.
68    /// * `target` - Type requested by the read operation.
69    ///
70    /// # Returns
71    ///
72    /// A descriptor classified as [`ValueMissingReason::UnsetScalar`].
73    #[must_use = "the unset scalar descriptor should be inspected or returned"]
74    #[inline(always)]
75    pub const fn unset_scalar(source: DataType, target: DataType) -> Self {
76        Self::storage(ValueMissingReason::UnsetScalar, source, target)
77    }
78
79    /// Creates an unset collection descriptor for the requested element type.
80    ///
81    /// # Parameters
82    ///
83    /// * `source` - Runtime element type of the unset collection storage.
84    /// * `target` - Element type requested by the read operation.
85    ///
86    /// # Returns
87    ///
88    /// A descriptor classified as [`ValueMissingReason::UnsetCollection`].
89    #[must_use = "the unset collection descriptor should be inspected or returned"]
90    #[inline(always)]
91    pub const fn unset_collection(source: DataType, target: DataType) -> Self {
92        Self::storage(ValueMissingReason::UnsetCollection, source, target)
93    }
94
95    /// Creates a descriptor for a first-item read from a concrete empty
96    /// collection.
97    ///
98    /// # Parameters
99    ///
100    /// * `source` - Runtime element type of the empty collection.
101    /// * `target` - Element type requested by the read operation.
102    ///
103    /// # Returns
104    ///
105    /// A descriptor classified as [`ValueMissingReason::EmptyCollection`].
106    #[must_use = "the empty collection descriptor should be inspected or returned"]
107    #[inline(always)]
108    pub const fn empty_collection(source: DataType, target: DataType) -> Self {
109        Self::storage(ValueMissingReason::EmptyCollection, source, target)
110    }
111
112    /// Records a known storage state without inventing a conversion source.
113    ///
114    /// # Parameters
115    ///
116    /// * `reason` - Storage-state classification for the missing read.
117    /// * `source` - Runtime type recorded by the source container.
118    /// * `target` - Type requested by the read operation.
119    ///
120    /// # Returns
121    ///
122    /// A descriptor with known source and target types and no item index.
123    #[must_use = "the storage descriptor should be retained"]
124    #[inline(always)]
125    const fn storage(reason: ValueMissingReason, source: DataType, target: DataType) -> Self {
126        Self {
127            reason,
128            source_type: Some(source),
129            target_type: Some(target),
130            source_index: None,
131            #[cfg(feature = "converter")]
132            conversion_error: None,
133        }
134    }
135
136    /// Preserves an already classified missing conversion and its original
137    /// index.
138    ///
139    /// # Parameters
140    ///
141    /// * `error` - Conversion error carrying the source and target facts.
142    /// * `source_index` - Original item index, or `None` for scalar conversion.
143    ///
144    /// # Returns
145    ///
146    /// A descriptor retaining the conversion error and collection position.
147    #[cfg(feature = "converter")]
148    #[must_use = "the conversion descriptor should be retained"]
149    #[inline]
150    pub(crate) fn from_conversion(error: DataConversionError, source_index: Option<usize>) -> Self {
151        Self {
152            reason: if error.kind() == DataConversionErrorKind::EmptyCollection {
153                ValueMissingReason::EmptyCollection
154            } else {
155                ValueMissingReason::Conversion
156            },
157            source_type: error.from_type(),
158            target_type: Some(error.to_type()),
159            source_index,
160            conversion_error: Some(error),
161        }
162    }
163
164    /// Enriches a conversion failure with facts known by its owning container.
165    ///
166    /// Only called after conversion admission has produced a missing error.
167    ///
168    /// # Parameters
169    ///
170    /// * `source` - Runtime type supplied by the owning container.
171    /// * `reason` - Storage classification supplied by that container.
172    ///
173    /// # Returns
174    ///
175    /// This descriptor with its source type and reason replaced.
176    #[cfg(feature = "converter")]
177    #[must_use = "the enriched descriptor should replace the original"]
178    #[inline(always)]
179    pub(crate) fn with_storage(mut self, source: DataType, reason: ValueMissingReason) -> Self {
180        self.source_type = Some(source);
181        self.reason = reason;
182        self
183    }
184
185    /// Records the first collection item's original index when conversion lost
186    /// it.
187    ///
188    /// # Returns
189    ///
190    /// This descriptor with index zero recorded when it represents an
191    /// unindexed conversion failure; all other descriptors are unchanged.
192    #[cfg(feature = "converter")]
193    #[must_use = "the indexed descriptor should replace the original"]
194    #[inline]
195    pub(crate) fn with_first_index(mut self) -> Self {
196        if self.reason == ValueMissingReason::Conversion && self.source_index.is_none() {
197            self.source_index = Some(0);
198        }
199        self
200    }
201
202    /// Returns the storage or policy reason for this missing result.
203    ///
204    /// This value is always present, including when the source or target type
205    /// is unknown.
206    ///
207    /// # Returns
208    ///
209    /// The storage or conversion-policy classification.
210    #[must_use]
211    #[inline(always)]
212    pub const fn reason(&self) -> ValueMissingReason {
213        self.reason
214    }
215
216    /// Returns the known source type, or `None` when conversion did not retain
217    /// one (for example, a generic empty iterator).
218    ///
219    /// # Returns
220    ///
221    /// `Some` with the stored source type when known; otherwise `None`.
222    #[must_use]
223    #[inline(always)]
224    pub const fn source_type(&self) -> Option<DataType> {
225        self.source_type
226    }
227
228    /// Returns the requested target type when known. Strict reads normally
229    /// provide it; a low-level conversion failure may leave it absent.
230    ///
231    /// # Returns
232    ///
233    /// `Some` with the requested type when known; otherwise `None`.
234    #[must_use]
235    #[inline(always)]
236    pub const fn target_type(&self) -> Option<DataType> {
237        self.target_type
238    }
239
240    /// Returns the original collection item index, or `None` for an outer
241    /// failure or a scalar conversion.
242    ///
243    /// # Returns
244    ///
245    /// `Some` with the zero-based source index for an item failure; otherwise
246    /// `None`.
247    #[must_use]
248    #[inline(always)]
249    pub const fn source_index(&self) -> Option<usize> {
250        self.source_index
251    }
252
253    /// Returns the original conversion error, absent for a strict storage
254    /// read. The error is the source for [`std::error::Error::source`].
255    ///
256    /// # Returns
257    ///
258    /// `Some` with the preserved conversion error for conversion failures;
259    /// otherwise `None`.
260    #[cfg(feature = "converter")]
261    #[must_use]
262    #[inline(always)]
263    pub const fn conversion_error(&self) -> Option<&DataConversionError> {
264        self.conversion_error.as_ref()
265    }
266
267    /// Reports whether scalar or collection storage is unset.
268    ///
269    /// This predicate is the condition used by strict fallback helpers.
270    ///
271    /// # Returns
272    ///
273    /// `true` for unset scalar or collection storage; otherwise `false`.
274    #[must_use]
275    #[inline(always)]
276    pub const fn is_unset(&self) -> bool {
277        matches!(
278            self.reason,
279            ValueMissingReason::UnsetScalar | ValueMissingReason::UnsetCollection
280        )
281    }
282
283    /// Reports whether a first-item read failed because a concrete collection
284    /// is empty.
285    ///
286    /// # Returns
287    ///
288    /// `true` only for a concrete empty collection; otherwise `false`.
289    #[must_use]
290    #[inline(always)]
291    pub const fn is_empty_collection(&self) -> bool {
292        matches!(self.reason, ValueMissingReason::EmptyCollection)
293    }
294
295    /// Reports whether conversion produced this failure, including enriched
296    /// unset states.
297    ///
298    /// # Returns
299    ///
300    /// `true` when a conversion error is represented or retained; otherwise
301    /// `false`.
302    #[must_use]
303    #[inline]
304    pub const fn is_conversion(&self) -> bool {
305        if matches!(self.reason, ValueMissingReason::Conversion) {
306            return true;
307        }
308        #[cfg(feature = "converter")]
309        {
310            self.conversion_error.is_some()
311        }
312        #[cfg(not(feature = "converter"))]
313        {
314            false
315        }
316    }
317
318    /// Allows strict fallback only for unset storage after the type check
319    /// passed. Concrete empty collections and type mismatches return `false`.
320    ///
321    /// # Returns
322    ///
323    /// `true` when a strict read may use its caller-supplied default.
324    #[must_use]
325    #[inline(always)]
326    pub const fn is_defaultable_for_strict_read(&self) -> bool {
327        self.is_unset()
328    }
329
330    /// Allows conversion fallback for unset storage or a policy-missing scalar.
331    ///
332    /// Empty collections and missing collection items never default.
333    ///
334    /// # Returns
335    ///
336    /// `true` when a conversion read may use its caller-supplied default.
337    #[must_use]
338    #[inline]
339    pub const fn is_defaultable_for_conversion(&self) -> bool {
340        self.is_unset() || (matches!(self.reason, ValueMissingReason::Conversion) && self.source_index.is_none())
341    }
342}
343
344impl fmt::Display for ValueMissing {
345    /// Formats diagnostic types and indices without exposing source payloads.
346    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
347        write!(
348            formatter,
349            "{:?}: source {:?}, target {:?}",
350            self.reason, self.source_type, self.target_type
351        )?;
352        if let Some(index) = self.source_index {
353            write!(formatter, ", collection index {index}")?;
354        }
355        Ok(())
356    }
357}
358
359impl std::error::Error for ValueMissing {
360    /// Exposes the preserved conversion error, or terminates a strict-read
361    /// chain.
362    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
363        #[cfg(feature = "converter")]
364        {
365            self.conversion_error
366                .as_ref()
367                .map(|error| error as &dyn std::error::Error)
368        }
369        #[cfg(not(feature = "converter"))]
370        {
371            None
372        }
373    }
374}