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    /// Storage state or conversion-policy classification of the missing read.
55    reason: ValueMissingReason,
56    /// Runtime source type, absent when the originating operation did not
57    /// retain it.
58    source_type: Option<DataType>,
59    /// Requested target type, absent when no target information is available.
60    target_type: Option<DataType>,
61    /// Zero-based original collection position, absent for scalar or outer
62    /// failures.
63    source_index: Option<usize>,
64    /// Owned conversion cause, absent for descriptors created by strict storage
65    /// reads.
66    #[cfg(feature = "converter")]
67    conversion_error: Option<DataConversionError>,
68}
69
70impl ValueMissing {
71    /// Creates an unset scalar descriptor for a read from `source` to `target`.
72    ///
73    /// # Parameters
74    ///
75    /// * `source` - Runtime type of the unset scalar storage.
76    /// * `target` - Type requested by the read operation.
77    ///
78    /// # Returns
79    ///
80    /// A descriptor classified as [`ValueMissingReason::UnsetScalar`].
81    #[inline(always)]
82    pub const fn unset_scalar(source: DataType, target: DataType) -> Self {
83        Self::storage(ValueMissingReason::UnsetScalar, source, target)
84    }
85
86    /// Creates an unset collection descriptor for the requested element type.
87    ///
88    /// # Parameters
89    ///
90    /// * `source` - Runtime element type of the unset collection storage.
91    /// * `target` - Element type requested by the read operation.
92    ///
93    /// # Returns
94    ///
95    /// A descriptor classified as [`ValueMissingReason::UnsetCollection`].
96    #[inline(always)]
97    pub const fn unset_collection(source: DataType, target: DataType) -> Self {
98        Self::storage(ValueMissingReason::UnsetCollection, source, target)
99    }
100
101    /// Creates a descriptor for a first-item read from a concrete empty
102    /// collection.
103    ///
104    /// # Parameters
105    ///
106    /// * `source` - Runtime element type of the empty collection.
107    /// * `target` - Element type requested by the read operation.
108    ///
109    /// # Returns
110    ///
111    /// A descriptor classified as [`ValueMissingReason::EmptyCollection`].
112    #[inline(always)]
113    pub const fn empty_collection(source: DataType, target: DataType) -> Self {
114        Self::storage(ValueMissingReason::EmptyCollection, source, target)
115    }
116
117    /// Preserves an already classified missing conversion and its original
118    /// index.
119    ///
120    /// # Parameters
121    ///
122    /// * `error` - Conversion error carrying the source and target facts.
123    /// * `source_index` - Original item index, or `None` for scalar conversion.
124    ///
125    /// # Returns
126    ///
127    /// A descriptor retaining the conversion error and collection position.
128    #[cfg(feature = "converter")]
129    #[inline]
130    pub(crate) fn from_conversion(error: DataConversionError, source_index: Option<usize>) -> Self {
131        Self {
132            reason: if error.kind() == DataConversionErrorKind::EmptyCollection {
133                ValueMissingReason::EmptyCollection
134            } else {
135                ValueMissingReason::Conversion
136            },
137            source_type: error.from_type(),
138            target_type: Some(error.to_type()),
139            source_index,
140            conversion_error: Some(error),
141        }
142    }
143
144    /// Records a known storage state without inventing a conversion source.
145    ///
146    /// # Parameters
147    ///
148    /// * `reason` - Storage-state classification for the missing read.
149    /// * `source` - Runtime type recorded by the source container.
150    /// * `target` - Type requested by the read operation.
151    ///
152    /// # Returns
153    ///
154    /// A descriptor with known source and target types and no item index.
155    #[inline(always)]
156    const fn storage(reason: ValueMissingReason, source: DataType, target: DataType) -> Self {
157        Self {
158            reason,
159            source_type: Some(source),
160            target_type: Some(target),
161            source_index: None,
162            #[cfg(feature = "converter")]
163            conversion_error: None,
164        }
165    }
166
167    /// Returns the storage or policy reason for this missing result.
168    ///
169    /// This value is always present, including when the source or target type
170    /// is unknown.
171    ///
172    /// # Returns
173    ///
174    /// The storage or conversion-policy classification.
175    #[must_use]
176    #[inline(always)]
177    pub const fn reason(&self) -> ValueMissingReason {
178        self.reason
179    }
180
181    /// Returns the known source type, or `None` when conversion did not retain
182    /// one (for example, a generic empty iterator).
183    ///
184    /// # Returns
185    ///
186    /// `Some` with the stored source type when known; otherwise `None`.
187    #[must_use]
188    #[inline(always)]
189    pub const fn source_type(&self) -> Option<DataType> {
190        self.source_type
191    }
192
193    /// Returns the requested target type when known. Strict reads normally
194    /// provide it; a low-level conversion failure may leave it absent.
195    ///
196    /// # Returns
197    ///
198    /// `Some` with the requested type when known; otherwise `None`.
199    #[must_use]
200    #[inline(always)]
201    pub const fn target_type(&self) -> Option<DataType> {
202        self.target_type
203    }
204
205    /// Returns the original collection item index, or `None` for an outer
206    /// failure or a scalar conversion.
207    ///
208    /// # Returns
209    ///
210    /// `Some` with the zero-based source index for an item failure; otherwise
211    /// `None`.
212    #[must_use]
213    #[inline(always)]
214    pub const fn source_index(&self) -> Option<usize> {
215        self.source_index
216    }
217
218    /// Returns the original conversion error, absent for a strict storage
219    /// read. The error is the source for [`std::error::Error::source`].
220    ///
221    /// # Returns
222    ///
223    /// `Some` with the preserved conversion error for conversion failures;
224    /// otherwise `None`.
225    #[cfg(feature = "converter")]
226    #[must_use]
227    #[inline(always)]
228    #[cfg_attr(docsrs, doc(cfg(feature = "converter")))]
229    pub const fn conversion_error(&self) -> Option<&DataConversionError> {
230        self.conversion_error.as_ref()
231    }
232
233    /// Reports whether scalar or collection storage is unset.
234    ///
235    /// This predicate is the condition used by strict fallback helpers.
236    ///
237    /// # Returns
238    ///
239    /// `true` for unset scalar or collection storage; otherwise `false`.
240    #[must_use]
241    #[inline(always)]
242    pub const fn is_unset(&self) -> bool {
243        matches!(
244            self.reason,
245            ValueMissingReason::UnsetScalar | ValueMissingReason::UnsetCollection
246        )
247    }
248
249    /// Reports whether a first-item read failed because a concrete collection
250    /// is empty.
251    ///
252    /// # Returns
253    ///
254    /// `true` only for a concrete empty collection; otherwise `false`.
255    #[must_use]
256    #[inline(always)]
257    pub const fn is_empty_collection(&self) -> bool {
258        matches!(self.reason, ValueMissingReason::EmptyCollection)
259    }
260
261    /// Reports whether conversion produced this failure, including enriched
262    /// unset states.
263    ///
264    /// # Returns
265    ///
266    /// `true` when a conversion error is represented or retained; otherwise
267    /// `false`.
268    #[must_use]
269    #[inline]
270    pub const fn is_conversion(&self) -> bool {
271        if matches!(self.reason, ValueMissingReason::Conversion) {
272            return true;
273        }
274        #[cfg(feature = "converter")]
275        {
276            self.conversion_error.is_some()
277        }
278        #[cfg(not(feature = "converter"))]
279        {
280            false
281        }
282    }
283
284    /// Reports whether the missing reason is unset storage.
285    ///
286    /// Strict readers must perform type admission before using this result
287    /// to select a default. This predicate does not compare `source_type`
288    /// with `target_type`; an unset descriptor constructed with different
289    /// source and target types still returns `true`.
290    /// Concrete empty collections and missing collection items return `false`.
291    ///
292    /// # Returns
293    ///
294    /// `true` for unset storage, allowing a strict fallback only after the
295    /// caller has separately accepted the requested type.
296    #[must_use]
297    #[inline(always)]
298    pub const fn is_defaultable_for_strict_read(&self) -> bool {
299        self.is_unset()
300    }
301
302    /// Allows conversion fallback for unset storage or a policy-missing scalar.
303    ///
304    /// Empty collections and missing collection items never default.
305    ///
306    /// # Returns
307    ///
308    /// `true` when a conversion read may use its caller-supplied default.
309    #[must_use]
310    #[inline]
311    pub const fn is_defaultable_for_conversion(&self) -> bool {
312        self.is_unset() || (matches!(self.reason, ValueMissingReason::Conversion) && self.source_index.is_none())
313    }
314
315    /// Enriches a conversion failure with facts known by its owning container.
316    ///
317    /// Only called after conversion admission has produced a missing error.
318    ///
319    /// # Parameters
320    ///
321    /// * `source` - Runtime type supplied by the owning container.
322    /// * `reason` - Storage classification supplied by that container.
323    ///
324    /// # Returns
325    ///
326    /// This descriptor with its source type and reason replaced.
327    #[cfg(feature = "converter")]
328    #[inline(always)]
329    pub(crate) fn with_storage(mut self, source: DataType, reason: ValueMissingReason) -> Self {
330        self.source_type = Some(source);
331        self.reason = reason;
332        self
333    }
334
335    /// Records the first collection item's original index when conversion lost
336    /// it.
337    ///
338    /// # Returns
339    ///
340    /// This descriptor with index zero recorded when it represents an
341    /// unindexed conversion failure; all other descriptors are unchanged.
342    #[cfg(feature = "converter")]
343    #[inline]
344    pub(crate) fn with_first_index(mut self) -> Self {
345        if self.reason == ValueMissingReason::Conversion && self.source_index.is_none() {
346            self.source_index = Some(0);
347        }
348        self
349    }
350}
351
352impl fmt::Display for ValueMissing {
353    /// Formats diagnostic types and indices without exposing source payloads.
354    ///
355    /// # Parameters
356    ///
357    /// * `formatter` - Destination and formatting options supplied by the
358    ///   caller.
359    ///
360    /// # Returns
361    ///
362    /// `Ok(())` when the diagnostic was written successfully.
363    ///
364    /// # Errors
365    ///
366    /// Returns an error if the destination rejects a formatted write.
367    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
368        write!(
369            formatter,
370            "{:?}: source {:?}, target {:?}",
371            self.reason, self.source_type, self.target_type
372        )?;
373        if let Some(index) = self.source_index {
374            write!(formatter, ", collection index {index}")?;
375        }
376        Ok(())
377    }
378}
379
380impl std::error::Error for ValueMissing {
381    /// Exposes the preserved conversion error, or terminates a strict-read
382    /// chain.
383    ///
384    /// # Returns
385    ///
386    /// `Some` borrowing the retained conversion cause when present; `None` for
387    /// strict storage reads or when the `converter` feature is disabled.
388    #[inline]
389    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
390        #[cfg(feature = "converter")]
391        {
392            self.conversion_error
393                .as_ref()
394                .map(|error| error as &dyn std::error::Error)
395        }
396        #[cfg(not(feature = "converter"))]
397        {
398            None
399        }
400    }
401}