Skip to main content

qubit_codec/transcode/
transcode_domain_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//! Domain-specific transcode errors with codec phase context.
9
10use core::num::NonZeroUsize;
11
12use thiserror::Error;
13
14/// Domain-specific codec, charset, or policy error with transcode context.
15///
16/// Reset and finish failures are lifecycle failures and therefore cannot carry
17/// an input index. Main-phase failures carry an input index whose coordinate
18/// meaning is defined by the concrete adapter. For codec-backed converters,
19/// ordinary decoded values retain their index in the input slice supplied to
20/// the decode call that produced them. A pending value keeps that index across
21/// calls; it is not rebased to a later input slice and is not a whole-stream
22/// offset. The retained index may be outside the bounds of a later input
23/// slice. Lifecycle-generated values encoded during reset or finish use
24/// placeholder index 0 because they have no source-unit position. Their encode
25/// error still has the main phase because it describes the underlying encode
26/// operation.
27///
28/// # Type Parameters
29///
30/// - `E`: Domain error reported by the concrete codec, hook, or facade.
31///
32/// # Examples
33///
34/// ```
35/// use qubit_codec::TranscodeDomainError;
36///
37/// let error = TranscodeDomainError::main("invalid byte", 4);
38/// assert_eq!(error.input_index(), Some(4));
39/// assert_eq!(error.to_string(), "codec main error at input index 4: invalid byte");
40/// ```
41#[derive(Clone, Copy, Debug, Eq, Error, Hash, PartialEq)]
42#[must_use]
43pub enum TranscodeDomainError<E> {
44    /// Domain error reported while resetting codec or hook state.
45    #[error("codec reset error: {source}")]
46    Reset {
47        /// Domain error returned by the codec or policy facade.
48        #[source]
49        source: E,
50    },
51
52    /// Domain error reported while processing input.
53    #[error("codec main error at input index {input_index}: {source}")]
54    Main {
55        /// Domain error returned by the codec or policy facade.
56        #[source]
57        source: E,
58        /// Input index where the error occurred, in the coordinate system
59        /// defined by the concrete adapter.
60        input_index: usize,
61        /// Invalid input units consumed by the codec-domain error, when known.
62        input_consumed: Option<NonZeroUsize>,
63    },
64
65    /// Domain error reported while finishing codec or hook state.
66    #[error("codec finish error: {source}")]
67    Finish {
68        /// Domain error returned by the codec or policy facade.
69        #[source]
70        source: E,
71    },
72}
73
74impl<E> TranscodeDomainError<E> {
75    /// Creates a reset-phase domain error.
76    ///
77    /// # Parameters
78    ///
79    /// - `source`: Domain error returned by reset handling.
80    ///
81    /// # Returns
82    ///
83    /// Returns a reset-phase domain error.
84    #[inline]
85    pub const fn reset(source: E) -> Self {
86        Self::Reset { source }
87    }
88
89    /// Creates a main-phase domain error.
90    ///
91    /// # Parameters
92    ///
93    /// - `source`: Domain error returned by value processing.
94    /// - `input_index`: Input index where the error occurred, in the coordinate
95    ///   system defined by the concrete adapter.
96    ///
97    /// # Returns
98    ///
99    /// Returns a main-phase domain error.
100    #[inline]
101    pub const fn main(source: E, input_index: usize) -> Self {
102        Self::Main {
103            source,
104            input_index,
105            input_consumed: None,
106        }
107    }
108
109    /// Creates a main-phase domain error with decode consumption context.
110    ///
111    /// # Parameters
112    ///
113    /// - `source`: Domain error returned by value processing.
114    /// - `input_index`: Input index where the error occurred, in the coordinate
115    ///   system defined by the concrete adapter.
116    /// - `input_consumed`: Invalid input units consumed, when known.
117    ///
118    /// # Returns
119    ///
120    /// Returns a main-phase domain error with consumption context.
121    #[inline]
122    pub const fn main_with_consumed(source: E, input_index: usize, input_consumed: Option<NonZeroUsize>) -> Self {
123        Self::Main {
124            source,
125            input_index,
126            input_consumed,
127        }
128    }
129
130    /// Creates a finish-phase domain error.
131    ///
132    /// # Parameters
133    ///
134    /// - `source`: Domain error returned by finish handling.
135    ///
136    /// # Returns
137    ///
138    /// Returns a finish-phase domain error.
139    #[inline]
140    pub const fn finish(source: E) -> Self {
141        Self::Finish { source }
142    }
143
144    /// Returns the wrapped source error.
145    ///
146    /// # Returns
147    ///
148    /// Returns a shared reference to the domain error.
149    #[inline]
150    #[must_use]
151    pub const fn source(&self) -> &E {
152        match self {
153            Self::Reset { source } | Self::Main { source, .. } | Self::Finish { source } => source,
154        }
155    }
156
157    /// Consumes this error and returns the wrapped source error.
158    ///
159    /// # Returns
160    ///
161    /// Returns the domain error.
162    #[inline]
163    #[must_use]
164    pub fn into_source(self) -> E {
165        match self {
166            Self::Reset { source } | Self::Main { source, .. } | Self::Finish { source } => source,
167        }
168    }
169
170    /// Returns the input index associated with the error, in the coordinate
171    /// system defined by the concrete adapter. The value may be a placeholder
172    /// when the adapter reports an error for a value without a source-unit
173    /// position.
174    ///
175    /// # Returns
176    ///
177    /// Returns `Some(index)` for main-phase errors and `None` for reset or
178    /// finish errors.
179    #[inline]
180    pub const fn input_index(&self) -> Option<usize> {
181        match self {
182            Self::Main { input_index, .. } => Some(*input_index),
183            Self::Reset { .. } | Self::Finish { .. } => None,
184        }
185    }
186
187    /// Returns invalid input consumption associated with the error.
188    ///
189    /// # Returns
190    ///
191    /// Returns `Some(consumed)` when a main-phase decode error reports the
192    /// invalid input width. Returns `None` otherwise.
193    #[inline]
194    pub const fn input_consumed(&self) -> Option<NonZeroUsize> {
195        match self {
196            Self::Main { input_consumed, .. } => *input_consumed,
197            Self::Reset { .. } | Self::Finish { .. } => None,
198        }
199    }
200
201    /// Maps the wrapped source error while preserving phase context.
202    ///
203    /// # Type Parameters
204    ///
205    /// - `T`: Target source error type.
206    /// - `F`: Mapping function type.
207    ///
208    /// # Parameters
209    ///
210    /// - `f`: Function applied to the wrapped source error.
211    ///
212    /// # Returns
213    ///
214    /// Returns the mapped domain error.
215    #[inline]
216    pub fn map_source<T, F>(self, f: F) -> TranscodeDomainError<T>
217    where
218        F: FnOnce(E) -> T,
219    {
220        match self {
221            Self::Reset { source } => TranscodeDomainError::Reset { source: f(source) },
222            Self::Main {
223                source,
224                input_index,
225                input_consumed,
226            } => TranscodeDomainError::Main {
227                source: f(source),
228                input_index,
229                input_consumed,
230            },
231            Self::Finish { source } => TranscodeDomainError::Finish { source: f(source) },
232        }
233    }
234}