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}