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 always carry the absolute input index
18/// where the domain error occurred.
19///
20/// # Type Parameters
21///
22/// - `E`: Domain error reported by the concrete codec, hook, or facade.
23///
24/// # Examples
25///
26/// ```
27/// use qubit_codec::TranscodeDomainError;
28///
29/// let error = TranscodeDomainError::main("invalid byte", 4);
30/// assert_eq!(error.input_index(), Some(4));
31/// assert_eq!(error.to_string(), "codec main error at input index 4: invalid byte");
32/// ```
33#[derive(Clone, Copy, Debug, Eq, Error, Hash, PartialEq)]
34#[must_use]
35pub enum TranscodeDomainError<E> {
36    /// Domain error reported while resetting codec or hook state.
37    #[error("codec reset error: {source}")]
38    Reset {
39        /// Domain error returned by the codec or policy facade.
40        #[source]
41        source: E,
42    },
43
44    /// Domain error reported while processing input.
45    #[error("codec main error at input index {input_index}: {source}")]
46    Main {
47        /// Domain error returned by the codec or policy facade.
48        #[source]
49        source: E,
50        /// Absolute input index where the error occurred.
51        input_index: usize,
52        /// Invalid input units consumed by the codec-domain error, when known.
53        input_consumed: Option<NonZeroUsize>,
54    },
55
56    /// Domain error reported while finishing codec or hook state.
57    #[error("codec finish error: {source}")]
58    Finish {
59        /// Domain error returned by the codec or policy facade.
60        #[source]
61        source: E,
62    },
63}
64
65impl<E> TranscodeDomainError<E> {
66    /// Creates a reset-phase domain error.
67    ///
68    /// # Parameters
69    ///
70    /// - `source`: Domain error returned by reset handling.
71    ///
72    /// # Returns
73    ///
74    /// Returns a reset-phase domain error.
75    #[inline]
76    pub const fn reset(source: E) -> Self {
77        Self::Reset { source }
78    }
79
80    /// Creates a main-phase domain error.
81    ///
82    /// # Parameters
83    ///
84    /// - `source`: Domain error returned by value processing.
85    /// - `input_index`: Absolute input index where the error occurred.
86    ///
87    /// # Returns
88    ///
89    /// Returns a main-phase domain error.
90    #[inline]
91    pub const fn main(source: E, input_index: usize) -> Self {
92        Self::Main {
93            source,
94            input_index,
95            input_consumed: None,
96        }
97    }
98
99    /// Creates a main-phase domain error with decode consumption context.
100    ///
101    /// # Parameters
102    ///
103    /// - `source`: Domain error returned by value processing.
104    /// - `input_index`: Absolute input index where the error occurred.
105    /// - `input_consumed`: Invalid input units consumed, when known.
106    ///
107    /// # Returns
108    ///
109    /// Returns a main-phase domain error with consumption context.
110    #[inline]
111    pub const fn main_with_consumed(source: E, input_index: usize, input_consumed: Option<NonZeroUsize>) -> Self {
112        Self::Main {
113            source,
114            input_index,
115            input_consumed,
116        }
117    }
118
119    /// Creates a finish-phase domain error.
120    ///
121    /// # Parameters
122    ///
123    /// - `source`: Domain error returned by finish handling.
124    ///
125    /// # Returns
126    ///
127    /// Returns a finish-phase domain error.
128    #[inline]
129    pub const fn finish(source: E) -> Self {
130        Self::Finish { source }
131    }
132
133    /// Returns the wrapped source error.
134    ///
135    /// # Returns
136    ///
137    /// Returns a shared reference to the domain error.
138    #[inline]
139    #[must_use]
140    pub const fn source(&self) -> &E {
141        match self {
142            Self::Reset { source } | Self::Main { source, .. } | Self::Finish { source } => source,
143        }
144    }
145
146    /// Consumes this error and returns the wrapped source error.
147    ///
148    /// # Returns
149    ///
150    /// Returns the domain error.
151    #[inline]
152    #[must_use]
153    pub fn into_source(self) -> E {
154        match self {
155            Self::Reset { source } | Self::Main { source, .. } | Self::Finish { source } => source,
156        }
157    }
158
159    /// Returns the absolute input index associated with the error.
160    ///
161    /// # Returns
162    ///
163    /// Returns `Some(index)` for main-phase errors and `None` for reset or
164    /// finish errors.
165    #[inline]
166    #[must_use]
167    pub const fn input_index(&self) -> Option<usize> {
168        match self {
169            Self::Main { input_index, .. } => Some(*input_index),
170            Self::Reset { .. } | Self::Finish { .. } => None,
171        }
172    }
173
174    /// Returns invalid input consumption associated with the error.
175    ///
176    /// # Returns
177    ///
178    /// Returns `Some(consumed)` when a main-phase decode error reports the
179    /// invalid input width. Returns `None` otherwise.
180    #[inline]
181    #[must_use]
182    pub const fn input_consumed(&self) -> Option<NonZeroUsize> {
183        match self {
184            Self::Main { input_consumed, .. } => *input_consumed,
185            Self::Reset { .. } | Self::Finish { .. } => None,
186        }
187    }
188
189    /// Maps the wrapped source error while preserving phase context.
190    ///
191    /// # Type Parameters
192    ///
193    /// - `T`: Target source error type.
194    /// - `F`: Mapping function type.
195    ///
196    /// # Parameters
197    ///
198    /// - `f`: Function applied to the wrapped source error.
199    ///
200    /// # Returns
201    ///
202    /// Returns the mapped domain error.
203    #[inline]
204    pub fn map_source<T, F>(self, f: F) -> TranscodeDomainError<T>
205    where
206        F: FnOnce(E) -> T,
207    {
208        match self {
209            Self::Reset { source } => TranscodeDomainError::Reset { source: f(source) },
210            Self::Main {
211                source,
212                input_index,
213                input_consumed,
214            } => TranscodeDomainError::Main {
215                source: f(source),
216                input_index,
217                input_consumed,
218            },
219            Self::Finish { source } => TranscodeDomainError::Finish { source: f(source) },
220        }
221    }
222}