Skip to main content

qubit_codec/transcode/
transcode_convert_error.rs

1// =============================================================================
2//    Copyright (c) 2026 Haixing Hu.
3//
4//    SPDX-License-Identifier: Apache-2.0
5//
6//    Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Errors reported by transcode converters.
9
10use thiserror::Error;
11
12use super::capacity_error::CapacityError;
13use super::transcode_decode_error::TranscodeDecodeError;
14use super::transcode_domain_error::TranscodeDomainError;
15use super::transcode_encode_error::TranscodeEncodeError;
16use super::transcode_failure::TranscodeFailure;
17use crate::Codec;
18
19/// Convert transcode error for a codec-backed converter.
20///
21/// # Type Parameters
22///
23/// - `D`: Source codec providing decode errors and the intermediate value type.
24/// - `E`: Target codec providing encode errors for the same value type.
25pub type TranscodeConvertErrorOf<D, E> =
26    TranscodeConvertError<<D as Codec>::DecodeError, <E as Codec>::EncodeError, <D as Codec>::Value>;
27
28/// Error reported by a unit-to-unit transcode conversion.
29///
30/// # Type Parameters
31///
32/// - `DE`: Source decoder domain error.
33/// - `EE`: Target encoder domain error.
34/// - `V`: Owned intermediate value retained when encoding is rejected.
35///
36/// # Examples
37///
38/// ```
39/// use qubit_codec::TranscodeConvertError;
40///
41/// let error = TranscodeConvertError::<&str, &str, u8>::unencodable(3, 42);
42/// assert_eq!(error.unencodable_ref(), Some((3, Some(&42))));
43/// assert_eq!(error.failure_ref(), None);
44/// ```
45#[derive(Clone, Debug, Eq, Error, Hash, PartialEq)]
46#[must_use]
47pub enum TranscodeConvertError<DE, EE, V> {
48    /// Framework-level transcode failure.
49    #[error(transparent)]
50    Failure(
51        /// Framework contract, capacity, or lifecycle failure.
52        #[from]
53        TranscodeFailure,
54    ),
55
56    /// Source-side domain error.
57    #[error("decode side failed: {0}")]
58    DecodeDomain(
59        /// Owned source error with its lifecycle phase and input context.
60        #[source]
61        TranscodeDomainError<DE>,
62    ),
63
64    /// Target-side domain error.
65    #[error("encode side failed: {0}")]
66    EncodeDomain(
67        /// Owned target error with its lifecycle phase and input context.
68        #[source]
69        TranscodeDomainError<EE>,
70    ),
71
72    /// The decoded intermediate value cannot be encoded by the target codec and
73    /// policy.
74    #[error("unencodable value at input index {input_index}")]
75    Unencodable {
76        /// Absolute source input index of the value being encoded.
77        input_index: usize,
78        /// `Some` retains the decoded intermediate value; `None` means value
79        /// context was unavailable or intentionally omitted.
80        value: Option<V>,
81    },
82}
83
84impl<DE, EE, V> TranscodeConvertError<DE, EE, V> {
85    /// Creates a source reset-phase domain-specific converter error.
86    ///
87    /// # Parameters
88    ///
89    /// - `source`: Owned source error raised during reset.
90    ///
91    /// # Returns
92    ///
93    /// Returns a source-domain reset error.
94    #[inline]
95    pub const fn decode_domain_reset(source: DE) -> Self {
96        Self::DecodeDomain(TranscodeDomainError::reset(source))
97    }
98
99    /// Creates a source main-phase domain-specific converter error.
100    ///
101    /// # Parameters
102    ///
103    /// - `source`: Owned source error raised during conversion.
104    /// - `input_index`: Absolute source position of the failed operation.
105    ///
106    /// # Returns
107    ///
108    /// Returns a source-domain main-phase error at the supplied input index.
109    #[inline]
110    pub const fn decode_domain_main(source: DE, input_index: usize) -> Self {
111        Self::DecodeDomain(TranscodeDomainError::main(source, input_index))
112    }
113
114    /// Creates a source main-phase converter error with decode consumption
115    /// context.
116    ///
117    /// # Parameters
118    ///
119    /// - `source`: Owned source error raised during conversion.
120    /// - `input_index`: Absolute source position of the failed operation.
121    /// - `input_consumed`: `Some` records invalid units consumed; `None` means
122    ///   unknown.
123    ///
124    /// # Returns
125    ///
126    /// Returns a source-domain main-phase error retaining consumption context.
127    #[inline]
128    pub const fn decode_domain_main_with_consumed(
129        source: DE,
130        input_index: usize,
131        input_consumed: Option<core::num::NonZeroUsize>,
132    ) -> Self {
133        Self::DecodeDomain(TranscodeDomainError::main_with_consumed(
134            source,
135            input_index,
136            input_consumed,
137        ))
138    }
139
140    /// Creates a source finish-phase domain-specific converter error.
141    ///
142    /// # Parameters
143    ///
144    /// - `source`: Owned source error raised during finalization.
145    ///
146    /// # Returns
147    ///
148    /// Returns a source-domain finish error.
149    #[inline]
150    pub const fn decode_domain_finish(source: DE) -> Self {
151        Self::DecodeDomain(TranscodeDomainError::finish(source))
152    }
153
154    /// Creates a target reset-phase domain-specific converter error.
155    ///
156    /// # Parameters
157    ///
158    /// - `source`: Owned target error raised during reset.
159    ///
160    /// # Returns
161    ///
162    /// Returns a target-domain reset error.
163    #[inline]
164    pub const fn encode_domain_reset(source: EE) -> Self {
165        Self::EncodeDomain(TranscodeDomainError::reset(source))
166    }
167
168    /// Creates a target main-phase domain-specific converter error.
169    ///
170    /// # Parameters
171    ///
172    /// - `source`: Owned target error raised during conversion.
173    /// - `input_index`: Absolute source position of the failed operation.
174    ///
175    /// # Returns
176    ///
177    /// Returns a target-domain main-phase error at the supplied input index.
178    #[inline]
179    pub const fn encode_domain_main(source: EE, input_index: usize) -> Self {
180        Self::EncodeDomain(TranscodeDomainError::main(source, input_index))
181    }
182
183    /// Creates a target finish-phase domain-specific converter error.
184    ///
185    /// # Parameters
186    ///
187    /// - `source`: Owned target error raised during finalization.
188    ///
189    /// # Returns
190    ///
191    /// Returns a target-domain finish error.
192    #[inline]
193    pub const fn encode_domain_finish(source: EE) -> Self {
194        Self::EncodeDomain(TranscodeDomainError::finish(source))
195    }
196
197    /// Creates an unencodable intermediate-value error with value context.
198    ///
199    /// # Parameters
200    ///
201    /// - `input_index`: Absolute source index of the rejected value.
202    /// - `value`: Intermediate value moved into the error for recovery.
203    ///
204    /// # Returns
205    ///
206    /// Returns an unencodable error owning `Some(value)`.
207    #[inline]
208    pub fn unencodable(input_index: usize, value: V) -> Self {
209        Self::Unencodable {
210            input_index,
211            value: Some(value),
212        }
213    }
214
215    /// Creates an unencodable intermediate-value error without value context.
216    ///
217    /// # Parameters
218    ///
219    /// - `input_index`: Absolute source index of the rejected value.
220    ///
221    /// # Returns
222    ///
223    /// Returns an unencodable error with absent (`None`) value context.
224    #[inline]
225    pub const fn unencodable_without_context(input_index: usize) -> Self {
226        Self::Unencodable {
227            input_index,
228            value: None,
229        }
230    }
231
232    /// Converts an encode error into a converter error while adding fallback
233    /// value context to unencodable errors that lack it.
234    ///
235    /// # Parameters
236    ///
237    /// - `error`: Owned target-side error to convert.
238    /// - `fallback_value`: Value moved into absent context, or dropped if not
239    ///   needed.
240    ///
241    /// # Returns
242    ///
243    /// Returns the converted error, adding the fallback only when unencodable
244    /// value context is absent.
245    #[inline]
246    pub fn from_encode_error_with_value(error: TranscodeEncodeError<EE, V>, fallback_value: V) -> Self {
247        match error {
248            TranscodeEncodeError::Unencodable {
249                input_index,
250                value: None,
251            } => Self::Unencodable {
252                input_index,
253                value: Some(fallback_value),
254            },
255            other => Self::from(other),
256        }
257    }
258
259    /// Returns the framework failure carried by this error.
260    ///
261    /// # Returns
262    ///
263    /// Returns `Some` for a framework failure, or `None` for domain and
264    /// unencodable errors.
265    #[inline]
266    #[must_use]
267    pub const fn failure_ref(&self) -> Option<&TranscodeFailure> {
268        match self {
269            Self::Failure(failure) => Some(failure),
270            Self::DecodeDomain(_) | Self::EncodeDomain(_) | Self::Unencodable { .. } => None,
271        }
272    }
273
274    /// Borrows the unencodable value context carried by this error.
275    ///
276    /// # Returns
277    ///
278    /// Returns `Some((index, value))` for an unencodable error and `None`
279    /// otherwise. The inner `Some` borrows retained value context; inner
280    /// `None` means it is absent.
281    #[inline]
282    #[must_use]
283    pub const fn unencodable_ref(&self) -> Option<(usize, Option<&V>)> {
284        match self {
285            Self::Unencodable { input_index, value } => Some((*input_index, value.as_ref())),
286            Self::Failure(_) | Self::DecodeDomain(_) | Self::EncodeDomain(_) => None,
287        }
288    }
289
290    /// Maps the source domain error while preserving other errors.
291    ///
292    /// # Type Parameters
293    ///
294    /// - `F`: One-shot transformation from `DE` to `T`.
295    /// - `T`: Replacement payload type.
296    ///
297    /// # Parameters
298    ///
299    /// - `f`: Invoked only for a source-domain error; otherwise dropped without
300    ///   being called.
301    ///
302    /// # Returns
303    ///
304    /// Returns an owned error preserving all other variants and positional
305    /// context.
306    ///
307    /// # Panics
308    ///
309    /// Propagates a panic if the invoked transformation panics.
310    #[inline]
311    pub fn map_decode_domain<F, T>(self, f: F) -> TranscodeConvertError<T, EE, V>
312    where
313        F: FnOnce(DE) -> T,
314    {
315        match self {
316            Self::Failure(failure) => TranscodeConvertError::Failure(failure),
317            Self::DecodeDomain(error) => TranscodeConvertError::DecodeDomain(error.map_source(f)),
318            Self::EncodeDomain(error) => TranscodeConvertError::EncodeDomain(error),
319            Self::Unencodable { input_index, value } => TranscodeConvertError::Unencodable { input_index, value },
320        }
321    }
322
323    /// Maps the target domain error while preserving other errors.
324    ///
325    /// # Type Parameters
326    ///
327    /// - `F`: One-shot transformation from `EE` to `T`.
328    /// - `T`: Replacement payload type.
329    ///
330    /// # Parameters
331    ///
332    /// - `f`: Invoked only for a target-domain error; otherwise dropped without
333    ///   being called.
334    ///
335    /// # Returns
336    ///
337    /// Returns an owned error preserving all other variants and positional
338    /// context.
339    ///
340    /// # Panics
341    ///
342    /// Propagates a panic if the invoked transformation panics.
343    #[inline]
344    pub fn map_encode_domain<F, T>(self, f: F) -> TranscodeConvertError<DE, T, V>
345    where
346        F: FnOnce(EE) -> T,
347    {
348        match self {
349            Self::Failure(failure) => TranscodeConvertError::Failure(failure),
350            Self::DecodeDomain(error) => TranscodeConvertError::DecodeDomain(error),
351            Self::EncodeDomain(error) => TranscodeConvertError::EncodeDomain(error.map_source(f)),
352            Self::Unencodable { input_index, value } => TranscodeConvertError::Unencodable { input_index, value },
353        }
354    }
355
356    /// Maps value context carried by unencodable-value errors.
357    ///
358    /// # Type Parameters
359    ///
360    /// - `F`: One-shot transformation from `V` to `W`.
361    /// - `W`: Replacement payload type.
362    ///
363    /// # Parameters
364    ///
365    /// - `f`: Invoked only for present unencodable value context; otherwise
366    ///   dropped without being called.
367    ///
368    /// # Returns
369    ///
370    /// Returns an owned error preserving all other variants and positional
371    /// context.
372    ///
373    /// # Panics
374    ///
375    /// Propagates a panic if the invoked transformation panics.
376    #[inline]
377    pub fn map_value<F, W>(self, f: F) -> TranscodeConvertError<DE, EE, W>
378    where
379        F: FnOnce(V) -> W,
380    {
381        match self {
382            Self::Failure(failure) => TranscodeConvertError::Failure(failure),
383            Self::DecodeDomain(error) => TranscodeConvertError::DecodeDomain(error),
384            Self::EncodeDomain(error) => TranscodeConvertError::EncodeDomain(error),
385            Self::Unencodable { input_index, value } => TranscodeConvertError::Unencodable {
386                input_index,
387                value: value.map(f),
388            },
389        }
390    }
391}
392
393impl<DE, EE, V> From<TranscodeDecodeError<DE>> for TranscodeConvertError<DE, EE, V> {
394    /// Converts a source-side decode error into a converter error.
395    ///
396    /// # Parameters
397    ///
398    /// - `error`: Owned failure to convert without discarding its context.
399    ///
400    /// # Returns
401    ///
402    /// Returns the corresponding converter variant preserving the original
403    /// cause.
404    #[inline]
405    fn from(error: TranscodeDecodeError<DE>) -> Self {
406        match error {
407            TranscodeDecodeError::Failure(failure) => Self::Failure(failure),
408            TranscodeDecodeError::Domain(error) => Self::DecodeDomain(error),
409        }
410    }
411}
412
413impl<DE, EE, V> From<TranscodeEncodeError<EE, V>> for TranscodeConvertError<DE, EE, V> {
414    /// Converts a target-side encode error into a converter error.
415    ///
416    /// # Parameters
417    ///
418    /// - `error`: Owned failure to convert without discarding its context.
419    ///
420    /// # Returns
421    ///
422    /// Returns the corresponding converter variant preserving the original
423    /// cause.
424    #[inline]
425    fn from(error: TranscodeEncodeError<EE, V>) -> Self {
426        match error {
427            TranscodeEncodeError::Failure(failure) => Self::Failure(failure),
428            TranscodeEncodeError::Unencodable { input_index, value } => Self::Unencodable { input_index, value },
429            TranscodeEncodeError::Domain(error) => Self::EncodeDomain(error),
430        }
431    }
432}
433
434impl<DE, EE, V> From<CapacityError> for TranscodeConvertError<DE, EE, V> {
435    /// Converts capacity planning errors into transcode framework errors.
436    ///
437    /// # Parameters
438    ///
439    /// - `error`: Owned failure to convert without discarding its context.
440    ///
441    /// # Returns
442    ///
443    /// Returns the corresponding converter variant preserving the original
444    /// cause.
445    #[inline]
446    fn from(error: CapacityError) -> Self {
447        TranscodeFailure::from(error).into()
448    }
449}