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}