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