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}