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 pub const fn input_index(&self) -> Option<usize> {
167 match self {
168 Self::Main { input_index, .. } => Some(*input_index),
169 Self::Reset { .. } | Self::Finish { .. } => None,
170 }
171 }
172
173 /// Returns invalid input consumption associated with the error.
174 ///
175 /// # Returns
176 ///
177 /// Returns `Some(consumed)` when a main-phase decode error reports the
178 /// invalid input width. Returns `None` otherwise.
179 #[inline]
180 pub const fn input_consumed(&self) -> Option<NonZeroUsize> {
181 match self {
182 Self::Main { input_consumed, .. } => *input_consumed,
183 Self::Reset { .. } | Self::Finish { .. } => None,
184 }
185 }
186
187 /// Maps the wrapped source error while preserving phase context.
188 ///
189 /// # Type Parameters
190 ///
191 /// - `T`: Target source error type.
192 /// - `F`: Mapping function type.
193 ///
194 /// # Parameters
195 ///
196 /// - `f`: Function applied to the wrapped source error.
197 ///
198 /// # Returns
199 ///
200 /// Returns the mapped domain error.
201 #[inline]
202 pub fn map_source<T, F>(self, f: F) -> TranscodeDomainError<T>
203 where
204 F: FnOnce(E) -> T,
205 {
206 match self {
207 Self::Reset { source } => TranscodeDomainError::Reset { source: f(source) },
208 Self::Main {
209 source,
210 input_index,
211 input_consumed,
212 } => TranscodeDomainError::Main {
213 source: f(source),
214 input_index,
215 input_consumed,
216 },
217 Self::Finish { source } => TranscodeDomainError::Finish { source: f(source) },
218 }
219 }
220}